From 682a7e182754188ce742879ff54d9ce99e9dbca2 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 17 Nov 2025 12:46:54 -0700 Subject: [PATCH 01/96] wip --- .../attachments/attachment_types.ts | 17 +++ .../onechat-common/attachments/attachments.ts | 1 + .../onechat-common/attachments/index.ts | 3 + .../services/attachments/definitions/alert.ts | 130 ++++++++++++++++++ .../services/attachments/definitions/index.ts | 2 + 5 files changed, 153 insertions(+) create mode 100644 x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/alert.ts diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts index 18e32ca2d028d..dce297588e138 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts @@ -16,12 +16,14 @@ export enum AttachmentType { screenContext = 'screen_context', text = 'text', esql = 'esql', + alert = 'alert', } interface AttachmentDataMap { [AttachmentType.esql]: EsqlAttachmentData; [AttachmentType.text]: TextAttachmentData; [AttachmentType.screenContext]: ScreenContextAttachmentData; + [AttachmentType.alert]: AlertAttachmentData; } export const esqlAttachmentDataSchema = z.object({ @@ -77,4 +79,19 @@ export interface ScreenContextAttachmentData { additional_data?: Record; } +export const alertAttachmentDataSchema = z.object({ + indexPattern: z.string(), + alert: z.string(), +}); + +/** + * Data for an alert attachment. + */ +export interface AlertAttachmentData { + /** The index pattern to search (e.g., `.alert-security.alerts-default*`) */ + indexPattern: string; + /** The condensed alert data in key-value format (comma-separated, newline-delimited) */ + alert: string; +} + export type AttachmentDataOf = AttachmentDataMap[Type]; diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts index a63cd31b4b76e..c28d22b702137 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts @@ -29,6 +29,7 @@ export interface Attachment< export type TextAttachment = Attachment; export type ScreenContextAttachment = Attachment; export type EsqlAttachment = Attachment; +export type AlertAttachment = Attachment; /** * Input version of an attachment, where the id is optional diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts index efe8c0d98169a..ebfa4d23ca13e 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts @@ -11,13 +11,16 @@ export type { TextAttachment, ScreenContextAttachment, EsqlAttachment, + AlertAttachment, } from './attachments'; export { AttachmentType, textAttachmentDataSchema, esqlAttachmentDataSchema, screenContextAttachmentDataSchema, + alertAttachmentDataSchema, type TextAttachmentData, type ScreenContextAttachmentData, type EsqlAttachmentData, + type AlertAttachmentData, } from './attachment_types'; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/alert.ts b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/alert.ts new file mode 100644 index 0000000000000..4fee99ba3c15f --- /dev/null +++ b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/alert.ts @@ -0,0 +1,130 @@ +/* + * 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. + */ + +import type { AlertAttachmentData } from '@kbn/onechat-common/attachments'; +import { AttachmentType, alertAttachmentDataSchema } from '@kbn/onechat-common/attachments'; +import { platformCoreTools } from '@kbn/onechat-common/tools'; +import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; + +/** + * Creates the definition for the `alert` attachment type. + */ +export const createAlertAttachmentType = (): AttachmentTypeDefinition< + AttachmentType.alert, + AlertAttachmentData +> => { + return { + id: AttachmentType.alert, + validate: (input) => { + const parseResult = alertAttachmentDataSchema.safeParse(input); + if (parseResult.success) { + return { valid: true, data: parseResult.data }; + } else { + return { valid: false, error: parseResult.error.message }; + } + }, + format: (attachment) => { + return { + getRepresentation: () => { + return { type: 'text', value: formatAlertData(attachment.data) }; + }, + }; + }, + getTools: () => [platformCoreTools.search], + getAgentDescription: () => { + return `Alert attachments contain security alert data. When using the search tool with alert attachments, use the "index" parameter to specify the index pattern. Do NOT include index patterns or "_index" filters in the query string itself. Before making additional searches, analyze existing alert data and previous search results to avoid redundant queries.`; + }, + }; +}; + +/** + * Essential fields to include in alert attachments to reduce token usage. + * These fields provide the most important context for security analysis. + */ +const ESSENTIAL_ALERT_FIELDS = new Set([ + '@timestamp', + 'kibana.alert.original_time', + 'kibana.alert.rule.name', + 'kibana.alert.rule.description', + 'kibana.alert.severity', + 'kibana.alert.risk_score', + 'kibana.alert.workflow_status', + 'host.name', + 'user.name', + 'source.ip', + 'destination.ip', + 'event.category', + 'event.action', + 'message', + '_id', +]); + +/** + * Filters alert data to include only essential fields to reduce token usage. + * + * @param alertData - The raw alert data string (comma-separated key-value pairs, newline-delimited) + * @returns Filtered alert data string with only essential fields + */ +const filterEssentialFields = (alertData: string): string => { + const lines = alertData.split('\n').filter((line) => line.trim().length > 0); + const filteredLines: string[] = []; + + for (const line of lines) { + const commaIndex = line.indexOf(','); + if (commaIndex > 0) { + const key = line.substring(0, commaIndex).trim(); + // Include essential fields and any field that starts with kibana.alert (for completeness) + if (ESSENTIAL_ALERT_FIELDS.has(key) || key.startsWith('kibana.alert.')) { + filteredLines.push(line); + } + } + } + + return filteredLines.join('\n'); +}; + +/** + * Formats alert data with minimal context about the index pattern. + * The alert data is filtered to include only essential fields to reduce token usage. + * + * @param data - The alert attachment data containing indexPattern and alert string + * @returns Formatted string representation of the alert + */ +const formatAlertData = (data: AlertAttachmentData): string => { + const filteredAlert = filterEssentialFields(data.alert); + + // Extract alert ID if available + const alertIdMatch = data.alert.match(/_id,([^\n]+)/); + const alertId = alertIdMatch ? alertIdMatch[1] : null; + + return `=== SECURITY ALERT DATA (ALREADY PROVIDED) === + +The following alert data is ALREADY available in this conversation. You do NOT need to query for this specific alert again. + +${filteredAlert} + +=== CRITICAL INSTRUCTIONS === + +1. **DO NOT query for this specific alert** - The data above is already provided. Any search query that would return this exact alert (e.g., matching host.name, user.name, source.ip, destination.ip, or _id) is REDUNDANT and should NOT be executed. + +2. **Only use the search tool to find RELATED data** such as: + - Other alerts from the same source IP, user, or host (but NOT this exact alert) + - Related events or logs from the same timeframe + - Additional context that supplements this alert + +3. **When searching**, you MUST: + - Pass the index pattern "${data.indexPattern}" as the "index" parameter + - Do NOT include "_index" or index patterns in your query string + - Use field filtering with KEEP command to reduce response size + - Exclude this alert's _id ("${alertId || 'N/A'}") from results if querying by other fields + +4. **Before making any search**, check if the information you need is already in the alert data above. + +=== INDEX PATTERN === +Use "${data.indexPattern}" as the index parameter when searching for related alerts.`; +}; + diff --git a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts index 4591a7e2d9666..2a11ca8877728 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts @@ -10,12 +10,14 @@ import type { AttachmentTypeRegistry } from '../attachment_type_registry'; import { createTextAttachmentType } from './text'; import { createEsqlAttachmentType } from './esql'; import { createScreenContextAttachmentType } from './screen_context'; +import { createAlertAttachmentType } from './alert'; export const registerAttachmentTypes = ({ registry }: { registry: AttachmentTypeRegistry }) => { const attachmentTypes: AttachmentTypeDefinition[] = [ createTextAttachmentType(), createScreenContextAttachmentType(), createEsqlAttachmentType(), + createAlertAttachmentType(), ]; attachmentTypes.forEach((attachmentType) => { From a15330a497589c9b7480dc25109470405335c584 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 17 Nov 2025 13:09:49 -0700 Subject: [PATCH 02/96] wip more, graph instructions --- .../onechat-genai-utils/tools/search/graph.ts | 35 +++++++++++++++++++ .../services/attachments/definitions/alert.ts | 27 ++++---------- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts index d983f85a2b773..797ebbb062641 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts @@ -62,6 +62,41 @@ export const createSearchToolGraph = ({ const selectAndValidateIndex = async (state: StateType) => { events?.reportProgress(progressMessages.selectingTarget()); + // If a specific index is provided (not a pattern with '*'), skip indexExplorer for performance + if (state.targetPattern && !state.targetPattern.includes('*') && state.targetPattern !== '*') { + logger?.info( + `[Search Tool] Using provided specific index directly: "${state.targetPattern}"` + ); + // Validate that the index exists by attempting to resolve it + try { + const resolveRes = await esClient.indices.resolveIndex({ + name: [state.targetPattern], + allow_no_indices: false, + }); + const resourceCount = + resolveRes.indices.length + resolveRes.aliases.length + resolveRes.data_streams.length; + if (resourceCount > 0) { + // Determine the type based on what was resolved + let resourceType: 'index' | 'alias' | 'data_stream' = 'index'; + if (resolveRes.aliases.length > 0) { + resourceType = 'alias'; + } else if (resolveRes.data_streams.length > 0) { + resourceType = 'data_stream'; + } + return { + indexIsValid: true, + searchTarget: { type: resourceType, name: state.targetPattern }, + }; + } + } catch (e) { + logger?.warn( + `[Search Tool] Failed to validate index "${state.targetPattern}", falling back to indexExplorer: ${e.message}` + ); + // Fall through to indexExplorer + } + } + + // Use indexExplorer for pattern matching or when specific index validation failed const explorerRes = await indexExplorer({ nlQuery: state.nlQuery, indexPattern: state.targetPattern ?? '*', diff --git a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/alert.ts b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/alert.ts index 4fee99ba3c15f..753c3a689be35 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/alert.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/alert.ts @@ -101,30 +101,17 @@ const formatAlertData = (data: AlertAttachmentData): string => { const alertIdMatch = data.alert.match(/_id,([^\n]+)/); const alertId = alertIdMatch ? alertIdMatch[1] : null; - return `=== SECURITY ALERT DATA (ALREADY PROVIDED) === - -The following alert data is ALREADY available in this conversation. You do NOT need to query for this specific alert again. + return `SECURITY ALERT DATA (ALREADY PROVIDED) ${filteredAlert} -=== CRITICAL INSTRUCTIONS === - -1. **DO NOT query for this specific alert** - The data above is already provided. Any search query that would return this exact alert (e.g., matching host.name, user.name, source.ip, destination.ip, or _id) is REDUNDANT and should NOT be executed. - -2. **Only use the search tool to find RELATED data** such as: - - Other alerts from the same source IP, user, or host (but NOT this exact alert) - - Related events or logs from the same timeframe - - Additional context that supplements this alert - -3. **When searching**, you MUST: - - Pass the index pattern "${data.indexPattern}" as the "index" parameter - - Do NOT include "_index" or index patterns in your query string - - Use field filtering with KEEP command to reduce response size - - Exclude this alert's _id ("${alertId || 'N/A'}") from results if querying by other fields +CRITICAL: This alert is already provided above. DO NOT query for this exact alert (matching host.name, user.name, source.ip, destination.ip, or _id="${alertId || 'N/A'}"). -4. **Before making any search**, check if the information you need is already in the alert data above. +ONLY search if the user explicitly requests related alerts or additional context. Do NOT automatically search for related data unless specifically asked. -=== INDEX PATTERN === -Use "${data.indexPattern}" as the index parameter when searching for related alerts.`; +When searching: +- Use index "${data.indexPattern}" as the "index" parameter (NOT in query string) +- Use KEEP command for field filtering to reduce response size +- Exclude _id="${alertId || 'N/A'}" from results`; }; From 7bc16c0115254f725d9608334cd6b268d317bcb4 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 17 Nov 2025 14:24:42 -0700 Subject: [PATCH 03/96] wip --- .../onechat/onechat-common/base/namespaces.ts | 2 + .../onechat-genai-utils/tools/search/graph.ts | 36 ----- .../services/attachments/definitions/index.ts | 2 - .../plugins/security_solution/kibana.jsonc | 3 +- .../agent_builder/attachments}/alert.ts | 16 +-- .../server/agent_builder/attachments/index.ts | 9 ++ .../agent_builder/tools/alerts/alerts_tool.ts | 136 ++++++++++++++++++ .../server/agent_builder/tools/constants.ts | 16 +++ .../server/agent_builder/tools/helpers.ts | 20 +++ .../server/agent_builder/tools/index.ts | 9 ++ .../security_solution/server/plugin.ts | 13 ++ .../server/plugin_contract.ts | 2 + .../plugins/security_solution/tsconfig.json | 5 +- 13 files changed, 221 insertions(+), 48 deletions(-) rename x-pack/{platform/plugins/shared/onechat/server/services/attachments/definitions => solutions/security/plugins/security_solution/server/agent_builder/attachments}/alert.ts (86%) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/base/namespaces.ts b/x-pack/platform/packages/shared/onechat/onechat-common/base/namespaces.ts index b5e2c4ba7a05d..a364591e6d53f 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/base/namespaces.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/base/namespaces.ts @@ -12,6 +12,7 @@ export const internalNamespaces = { platformCore: 'platform.core', observability: 'observability', + coreSecurity: 'core.security', } as const; /** @@ -20,6 +21,7 @@ export const internalNamespaces = { export const protectedNamespaces: string[] = [ internalNamespaces.platformCore, internalNamespaces.observability, + internalNamespaces.coreSecurity, ]; /** diff --git a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts index 797ebbb062641..b74d76cd90fce 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts @@ -61,42 +61,6 @@ export const createSearchToolGraph = ({ const selectAndValidateIndex = async (state: StateType) => { events?.reportProgress(progressMessages.selectingTarget()); - - // If a specific index is provided (not a pattern with '*'), skip indexExplorer for performance - if (state.targetPattern && !state.targetPattern.includes('*') && state.targetPattern !== '*') { - logger?.info( - `[Search Tool] Using provided specific index directly: "${state.targetPattern}"` - ); - // Validate that the index exists by attempting to resolve it - try { - const resolveRes = await esClient.indices.resolveIndex({ - name: [state.targetPattern], - allow_no_indices: false, - }); - const resourceCount = - resolveRes.indices.length + resolveRes.aliases.length + resolveRes.data_streams.length; - if (resourceCount > 0) { - // Determine the type based on what was resolved - let resourceType: 'index' | 'alias' | 'data_stream' = 'index'; - if (resolveRes.aliases.length > 0) { - resourceType = 'alias'; - } else if (resolveRes.data_streams.length > 0) { - resourceType = 'data_stream'; - } - return { - indexIsValid: true, - searchTarget: { type: resourceType, name: state.targetPattern }, - }; - } - } catch (e) { - logger?.warn( - `[Search Tool] Failed to validate index "${state.targetPattern}", falling back to indexExplorer: ${e.message}` - ); - // Fall through to indexExplorer - } - } - - // Use indexExplorer for pattern matching or when specific index validation failed const explorerRes = await indexExplorer({ nlQuery: state.nlQuery, indexPattern: state.targetPattern ?? '*', diff --git a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts index 2a11ca8877728..4591a7e2d9666 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts @@ -10,14 +10,12 @@ import type { AttachmentTypeRegistry } from '../attachment_type_registry'; import { createTextAttachmentType } from './text'; import { createEsqlAttachmentType } from './esql'; import { createScreenContextAttachmentType } from './screen_context'; -import { createAlertAttachmentType } from './alert'; export const registerAttachmentTypes = ({ registry }: { registry: AttachmentTypeRegistry }) => { const attachmentTypes: AttachmentTypeDefinition[] = [ createTextAttachmentType(), createScreenContextAttachmentType(), createEsqlAttachmentType(), - createAlertAttachmentType(), ]; attachmentTypes.forEach((attachmentType) => { diff --git a/x-pack/solutions/security/plugins/security_solution/kibana.jsonc b/x-pack/solutions/security/plugins/security_solution/kibana.jsonc index 9a9a232c9fb8b..3d025aded1f4e 100644 --- a/x-pack/solutions/security/plugins/security_solution/kibana.jsonc +++ b/x-pack/solutions/security/plugins/security_solution/kibana.jsonc @@ -76,7 +76,8 @@ "osquery", "savedObjectsTaggingOss", "automaticImport", - "serverless" + "serverless", + "onechat" ], "requiredBundles": [ "esUiShared", diff --git a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts similarity index 86% rename from x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/alert.ts rename to x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 753c3a689be35..b281133cb89d0 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -7,8 +7,8 @@ import type { AlertAttachmentData } from '@kbn/onechat-common/attachments'; import { AttachmentType, alertAttachmentDataSchema } from '@kbn/onechat-common/attachments'; -import { platformCoreTools } from '@kbn/onechat-common/tools'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import { SECURITY_ALERTS_TOOL_ID } from '../tools'; /** * Creates the definition for the `alert` attachment type. @@ -34,9 +34,9 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition< }, }; }, - getTools: () => [platformCoreTools.search], + getTools: () => [SECURITY_ALERTS_TOOL_ID], getAgentDescription: () => { - return `Alert attachments contain security alert data. When using the search tool with alert attachments, use the "index" parameter to specify the index pattern. Do NOT include index patterns or "_index" filters in the query string itself. Before making additional searches, analyze existing alert data and previous search results to avoid redundant queries.`; + return `Alert attachments contain security alert data. Use the alerts tool (${SECURITY_ALERTS_TOOL_ID}) to search for related alerts or additional context. The alerts tool automatically filters fields to reduce response size.`; }, }; }; @@ -96,22 +96,22 @@ const filterEssentialFields = (alertData: string): string => { */ const formatAlertData = (data: AlertAttachmentData): string => { const filteredAlert = filterEssentialFields(data.alert); - + // Extract alert ID if available const alertIdMatch = data.alert.match(/_id,([^\n]+)/); const alertId = alertIdMatch ? alertIdMatch[1] : null; - + return `SECURITY ALERT DATA (ALREADY PROVIDED) ${filteredAlert} -CRITICAL: This alert is already provided above. DO NOT query for this exact alert (matching host.name, user.name, source.ip, destination.ip, or _id="${alertId || 'N/A'}"). +CRITICAL: This alert is already provided above. DO NOT query for this exact alert (matching host.name, user.name, source.ip, destination.ip, or _id="${ + alertId || 'N/A' + }"). ONLY search if the user explicitly requests related alerts or additional context. Do NOT automatically search for related data unless specifically asked. When searching: - Use index "${data.indexPattern}" as the "index" parameter (NOT in query string) -- Use KEEP command for field filtering to reduce response size - Exclude _id="${alertId || 'N/A'}" from results`; }; - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts new file mode 100644 index 0000000000000..ac51b233318b4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { createAlertAttachmentType } from './alert'; + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts new file mode 100644 index 0000000000000..04b24c55c8790 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts @@ -0,0 +1,136 @@ +/* + * 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. + */ + +import { z } from '@kbn/zod'; +import { ToolType } from '@kbn/onechat-common'; +import { generateEsql } from '@kbn/onechat-genai-utils/tools'; +import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import { DEFAULT_ALERTS_INDEX } from '../../../../common/constants'; +import { getSpaceIdFromRequest } from '../helpers'; +import { securityTool } from '../constants'; + +const alertsSchema = z.object({ + query: z + .string() + .describe('A natural language query expressing the search request for security alerts'), + index: z + .string() + .optional() + .describe( + 'Specific alerts index to search against. If not provided, will search against .alerts-security.alerts-* pattern.' + ), +}); + +export const SECURITY_ALERTS_TOOL_ID = securityTool('alerts'); + +const KEEP_FIELDS = [ + '@timestamp', + 'host.name', + 'user.name', + 'kibana.alert.rule.name', + 'kibana.alert.severity', + 'kibana.alert.risk_score', + 'source.ip', + 'destination.ip', + 'event.category', + 'message', +].join(', '); + +const ADDITIONAL_INSTRUCTIONS = `When querying security alert indices, ALWAYS use the KEEP command to filter fields and reduce response size. Include these essential fields: ${KEEP_FIELDS}. Example: FROM .alerts-security.alerts-* | KEEP ${KEEP_FIELDS} | ...`; + +export const alertsTool = (): BuiltinToolDefinition => { + return { + id: SECURITY_ALERTS_TOOL_ID, + type: ToolType.builtin, + description: `Search and analyze security alerts using full-text or structured queries for finding, counting, aggregating, or summarizing alerts.`, + schema: alertsSchema, + handler: async ( + { query: nlQuery, index }, + { request, esClient, modelProvider, logger, events } + ) => { + // Determine the index to use: either explicitly provided or based on the current space + const spaceId = getSpaceIdFromRequest(request); + const searchIndex = index ?? `${DEFAULT_ALERTS_INDEX}-${spaceId}`; + + logger.debug(`alerts tool called with query: ${nlQuery}, index: ${searchIndex}`); + + try { + // Generate ES|QL query with automatic KEEP field filtering + const esqlResponse = await generateEsql({ + nlQuery, + index: searchIndex, + additionalInstructions: ADDITIONAL_INSTRUCTIONS, + executeQuery: true, + model: await modelProvider.getDefaultModel(), + esClient: esClient.asCurrentUser, + logger, + events, + }); + + if (esqlResponse.error) { + return { + results: [ + { + type: 'error', + data: { + message: esqlResponse.error, + }, + }, + ], + }; + } + + const results = []; + + if (esqlResponse.query) { + results.push({ + type: 'query', + data: { + esql: esqlResponse.query, + }, + }); + } + + if (esqlResponse.results) { + results.push({ + type: 'tabularData', + data: { + source: 'esql', + query: esqlResponse.query || '', + columns: esqlResponse.results.columns, + values: esqlResponse.results.values, + }, + }); + } + + if (esqlResponse.answer) { + results.push({ + type: 'other', + data: { + answer: esqlResponse.answer, + }, + }); + } + + return { results }; + } catch (error) { + logger.error(`Error in alerts tool: ${error.message}`); + return { + results: [ + { + type: 'error', + data: { + message: `Error: ${error.message}`, + }, + }, + ], + }; + } + }, + tags: ['security', 'alerts'], + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts new file mode 100644 index 0000000000000..85b61fe9b8951 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +import { internalNamespaces } from '@kbn/onechat-common/base/namespaces'; + +/** + * Creates a security tool ID with the core.security namespace. + */ +export const securityTool = (toolName: string): string => { + return `${internalNamespaces.coreSecurity}.${toolName}`; +}; + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.ts new file mode 100644 index 0000000000000..2f869a34027f6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +import type { KibanaRequest } from '@kbn/core-http-server'; +import { DEFAULT_SPACE_ID, getSpaceIdFromPath } from '@kbn/spaces-plugin/common'; + +/** + * Gets the space ID from the request path. + * Falls back to 'default' if no space ID is found in the path. + */ +export const getSpaceIdFromRequest = (request: KibanaRequest): string => { + const pathname = request.url.pathname; + const { spaceId } = getSpaceIdFromPath(pathname); + return spaceId ?? DEFAULT_SPACE_ID; +}; + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts new file mode 100644 index 0000000000000..e5d80bcb21421 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { alertsTool, SECURITY_ALERTS_TOOL_ID } from './alerts/alerts_tool'; + diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 5184abab328e7..58386fbe7d46d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -146,6 +146,8 @@ import { HealthDiagnosticServiceImpl } from './lib/telemetry/diagnostic/health_d import type { HealthDiagnosticService } from './lib/telemetry/diagnostic/health_diagnostic_service.types'; import { ENTITY_RISK_SCORE_TOOL_ID } from './assistant/tools/entity_risk_score/entity_risk_score'; import type { TelemetryQueryConfiguration } from './lib/telemetry/types'; +import { createAlertAttachmentType } from './agent_builder/attachments'; +import { alertsTool } from './agent_builder/tools'; export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract'; @@ -658,6 +660,17 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.elasticAssistant.registerFeatures(APP_UI_ID, features); plugins.elasticAssistant.registerFeatures('management', features); + // Register alert attachment type and alerts tool with onechat + // Note: This requires onechat to be added as a setup dependency + if (plugins.onechat) { + if (plugins.onechat.attachments) { + plugins.onechat.attachments.registerType(createAlertAttachmentType()); + } + if (plugins.onechat.tools) { + plugins.onechat.tools.register(alertsTool()); + } + } + const manifestManager = new ManifestManager({ savedObjectsClientFactory: new SavedObjectsClientFactory(core.savedObjects, core.http), savedObjectsClient, diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin_contract.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin_contract.ts index ffbd6cc5ea0b9..88dc75adbbb29 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin_contract.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin_contract.ts @@ -45,6 +45,7 @@ import type { SharePluginStart } from '@kbn/share-plugin/server'; import type { PluginSetup as UnifiedSearchServerPluginSetup } from '@kbn/unified-search-plugin/server'; import type { ElasticAssistantPluginStart } from '@kbn/elastic-assistant-plugin/server'; import type { InferenceServerStart } from '@kbn/inference-plugin/server'; +import type { OnechatPluginSetup } from '@kbn/onechat-plugin/server'; import type { ProductFeaturesService } from './lib/product_features_service/product_features_service'; import type { ExperimentalFeatures } from '../common'; @@ -68,6 +69,7 @@ export interface SecuritySolutionPluginSetupDependencies { licensing: LicensingPluginSetup; osquery: OsqueryPluginSetup; unifiedSearch: UnifiedSearchServerPluginSetup; + onechat?: OnechatPluginSetup; } export interface SecuritySolutionPluginStartDependencies { diff --git a/x-pack/solutions/security/plugins/security_solution/tsconfig.json b/x-pack/solutions/security/plugins/security_solution/tsconfig.json index 97d130e8008af..0f31955bad8b7 100644 --- a/x-pack/solutions/security/plugins/security_solution/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution/tsconfig.json @@ -262,6 +262,9 @@ "@kbn/react-query", "@kbn/core-chrome-layout-constants", "@kbn/response-ops-rule-form", - "@kbn/core-lifecycle-browser-mocks" + "@kbn/core-lifecycle-browser-mocks", + "@kbn/onechat-common", + "@kbn/spaces-utils", + "@kbn/onechat-plugin" ] } From 4ece3eddd4d5c6d46fd8a94ba0debb70cb35e337 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 17 Nov 2025 15:18:16 -0700 Subject: [PATCH 04/96] working? This reverts commit 7bc16c0115254f725d9608334cd6b268d317bcb4. --- .../onechat/onechat-server/allow_lists.ts | 2 ++ .../server/agent_builder/tools/index.ts | 1 - .../security_solution/server/plugin.ts | 35 +++++++++++++------ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts b/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts index ea9820a737bc2..94e524cc7dd09 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts @@ -18,6 +18,8 @@ export const AGENT_BUILDER_BUILTIN_TOOLS: string[] = [ 'observability.search_knowledge_base', 'observability.get_data_sources', 'observability.get_alerts', + // Security Solution + 'core.security.alerts', ]; /** diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts index e5d80bcb21421..29620b59b9c3f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts @@ -6,4 +6,3 @@ */ export { alertsTool, SECURITY_ALERTS_TOOL_ID } from './alerts/alerts_tool'; - diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 58386fbe7d46d..d6724dc6b1262 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -605,6 +605,30 @@ export class Plugin implements ISecuritySolutionPlugin { this.logger.warn('Task Manager not available, health diagnostic task not registered.'); } + // Register alert attachment type and alerts tool with onechat + // Note: This requires onechat to be added as an optional plugin dependency + // Note: The alert attachment type may already be registered by onechat's built-in types. + // If so, we'll skip registration and use the built-in version. + if (plugins.onechat) { + if (plugins.onechat.attachments) { + try { + plugins.onechat.attachments.registerType(createAlertAttachmentType()); + } catch (error) { + // Alert attachment type may already be registered by onechat's built-in types + if (error instanceof Error && error.message.includes('already registered')) { + this.logger.debug( + 'Alert attachment type already registered by onechat plugin, using built-in version' + ); + } else { + throw error; + } + } + } + if (plugins.onechat.tools) { + plugins.onechat.tools.register(alertsTool()); + } + } + return { setProductFeaturesConfigurator: productFeaturesService.setProductFeaturesConfigurator.bind(productFeaturesService), @@ -660,17 +684,6 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.elasticAssistant.registerFeatures(APP_UI_ID, features); plugins.elasticAssistant.registerFeatures('management', features); - // Register alert attachment type and alerts tool with onechat - // Note: This requires onechat to be added as a setup dependency - if (plugins.onechat) { - if (plugins.onechat.attachments) { - plugins.onechat.attachments.registerType(createAlertAttachmentType()); - } - if (plugins.onechat.tools) { - plugins.onechat.tools.register(alertsTool()); - } - } - const manifestManager = new ManifestManager({ savedObjectsClientFactory: new SavedObjectsClientFactory(core.savedObjects, core.http), savedObjectsClient, From be8e838b95ff33302903aa95094f53827b75a7c4 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 18 Nov 2025 08:19:48 -0700 Subject: [PATCH 05/96] idk --- .../attachments/attachment_types.ts | 3 - .../onechat/onechat-server/allow_lists.ts | 1 + .../server/agent_builder/attachments/alert.ts | 28 ++-- .../server/agent_builder/attachments/index.ts | 1 + .../tools/alerts/alerts_tool2.ts | 136 ++++++++++++++++++ .../tools/alerts/evaluate_alert_tool.ts | 121 ++++++++++++++++ .../agent_builder/tools/alerts/helpers.ts | 26 ++++ .../server/agent_builder/tools/constants.ts | 1 - .../server/agent_builder/tools/index.ts | 1 + .../security_solution/server/plugin.ts | 3 +- 10 files changed, 297 insertions(+), 24 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool2.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/helpers.ts diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts index dce297588e138..46924acb6c19f 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts @@ -80,7 +80,6 @@ export interface ScreenContextAttachmentData { } export const alertAttachmentDataSchema = z.object({ - indexPattern: z.string(), alert: z.string(), }); @@ -88,8 +87,6 @@ export const alertAttachmentDataSchema = z.object({ * Data for an alert attachment. */ export interface AlertAttachmentData { - /** The index pattern to search (e.g., `.alert-security.alerts-default*`) */ - indexPattern: string; /** The condensed alert data in key-value format (comma-separated, newline-delimited) */ alert: string; } diff --git a/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts b/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts index 94e524cc7dd09..38b52ee0d3efb 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts @@ -20,6 +20,7 @@ export const AGENT_BUILDER_BUILTIN_TOOLS: string[] = [ 'observability.get_alerts', // Security Solution 'core.security.alerts', + 'core.security.evaluate-alert', ]; /** diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index b281133cb89d0..4678171a40786 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -8,7 +8,7 @@ import type { AlertAttachmentData } from '@kbn/onechat-common/attachments'; import { AttachmentType, alertAttachmentDataSchema } from '@kbn/onechat-common/attachments'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; -import { SECURITY_ALERTS_TOOL_ID } from '../tools'; +import { EVALUATE_ALERT_TOOL_ID } from '../tools'; /** * Creates the definition for the `alert` attachment type. @@ -34,9 +34,9 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition< }, }; }, - getTools: () => [SECURITY_ALERTS_TOOL_ID], + getTools: () => [EVALUATE_ALERT_TOOL_ID], getAgentDescription: () => { - return `Alert attachments contain security alert data. Use the alerts tool (${SECURITY_ALERTS_TOOL_ID}) to search for related alerts or additional context. The alerts tool automatically filters fields to reduce response size.`; + return `Alert attachments contain security alert data. Use the ${EVALUATE_ALERT_TOOL_ID} tool to generate a comprehensive evaluation report. IMPORTANT: When the evaluation tool returns results, return them EXACTLY as provided without summarization or modification.`; }, }; }; @@ -88,30 +88,20 @@ const filterEssentialFields = (alertData: string): string => { }; /** - * Formats alert data with minimal context about the index pattern. + * Formats alert data for display. * The alert data is filtered to include only essential fields to reduce token usage. * - * @param data - The alert attachment data containing indexPattern and alert string - * @returns Formatted string representation of the alert + * @param data - The alert attachment data containing the alert string + * @returns Formatted string representation of the filtered alert data */ const formatAlertData = (data: AlertAttachmentData): string => { const filteredAlert = filterEssentialFields(data.alert); - // Extract alert ID if available - const alertIdMatch = data.alert.match(/_id,([^\n]+)/); - const alertId = alertIdMatch ? alertIdMatch[1] : null; - - return `SECURITY ALERT DATA (ALREADY PROVIDED) + return `SECURITY ALERT DATA ${filteredAlert} -CRITICAL: This alert is already provided above. DO NOT query for this exact alert (matching host.name, user.name, source.ip, destination.ip, or _id="${ - alertId || 'N/A' - }"). - -ONLY search if the user explicitly requests related alerts or additional context. Do NOT automatically search for related data unless specifically asked. +--- -When searching: -- Use index "${data.indexPattern}" as the "index" parameter (NOT in query string) -- Exclude _id="${alertId || 'N/A'}" from results`; +To evaluate this alert, use the ${EVALUATE_ALERT_TOOL_ID} tool with the alertData parameter set to the filtered alert data shown above.`; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts index ac51b233318b4..e04586e444515 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts @@ -7,3 +7,4 @@ export { createAlertAttachmentType } from './alert'; + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool2.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool2.ts new file mode 100644 index 0000000000000..04b24c55c8790 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool2.ts @@ -0,0 +1,136 @@ +/* + * 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. + */ + +import { z } from '@kbn/zod'; +import { ToolType } from '@kbn/onechat-common'; +import { generateEsql } from '@kbn/onechat-genai-utils/tools'; +import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import { DEFAULT_ALERTS_INDEX } from '../../../../common/constants'; +import { getSpaceIdFromRequest } from '../helpers'; +import { securityTool } from '../constants'; + +const alertsSchema = z.object({ + query: z + .string() + .describe('A natural language query expressing the search request for security alerts'), + index: z + .string() + .optional() + .describe( + 'Specific alerts index to search against. If not provided, will search against .alerts-security.alerts-* pattern.' + ), +}); + +export const SECURITY_ALERTS_TOOL_ID = securityTool('alerts'); + +const KEEP_FIELDS = [ + '@timestamp', + 'host.name', + 'user.name', + 'kibana.alert.rule.name', + 'kibana.alert.severity', + 'kibana.alert.risk_score', + 'source.ip', + 'destination.ip', + 'event.category', + 'message', +].join(', '); + +const ADDITIONAL_INSTRUCTIONS = `When querying security alert indices, ALWAYS use the KEEP command to filter fields and reduce response size. Include these essential fields: ${KEEP_FIELDS}. Example: FROM .alerts-security.alerts-* | KEEP ${KEEP_FIELDS} | ...`; + +export const alertsTool = (): BuiltinToolDefinition => { + return { + id: SECURITY_ALERTS_TOOL_ID, + type: ToolType.builtin, + description: `Search and analyze security alerts using full-text or structured queries for finding, counting, aggregating, or summarizing alerts.`, + schema: alertsSchema, + handler: async ( + { query: nlQuery, index }, + { request, esClient, modelProvider, logger, events } + ) => { + // Determine the index to use: either explicitly provided or based on the current space + const spaceId = getSpaceIdFromRequest(request); + const searchIndex = index ?? `${DEFAULT_ALERTS_INDEX}-${spaceId}`; + + logger.debug(`alerts tool called with query: ${nlQuery}, index: ${searchIndex}`); + + try { + // Generate ES|QL query with automatic KEEP field filtering + const esqlResponse = await generateEsql({ + nlQuery, + index: searchIndex, + additionalInstructions: ADDITIONAL_INSTRUCTIONS, + executeQuery: true, + model: await modelProvider.getDefaultModel(), + esClient: esClient.asCurrentUser, + logger, + events, + }); + + if (esqlResponse.error) { + return { + results: [ + { + type: 'error', + data: { + message: esqlResponse.error, + }, + }, + ], + }; + } + + const results = []; + + if (esqlResponse.query) { + results.push({ + type: 'query', + data: { + esql: esqlResponse.query, + }, + }); + } + + if (esqlResponse.results) { + results.push({ + type: 'tabularData', + data: { + source: 'esql', + query: esqlResponse.query || '', + columns: esqlResponse.results.columns, + values: esqlResponse.results.values, + }, + }); + } + + if (esqlResponse.answer) { + results.push({ + type: 'other', + data: { + answer: esqlResponse.answer, + }, + }); + } + + return { results }; + } catch (error) { + logger.error(`Error in alerts tool: ${error.message}`); + return { + results: [ + { + type: 'error', + data: { + message: `Error: ${error.message}`, + }, + }, + ], + }; + } + }, + tags: ['security', 'alerts'], + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts new file mode 100644 index 0000000000000..33c221c0b63ce --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts @@ -0,0 +1,121 @@ +/* + * 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. + */ + +import { z } from '@kbn/zod'; +import { ToolType } from '@kbn/onechat-common'; +import { HumanMessage } from '@langchain/core/messages'; +import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import { securityTool } from '../constants'; + +const evaluateAlertSchema = z.object({ + alertData: z + .string() + .describe( + 'The filtered alert data in key-value format (comma-separated, newline-delimited). Contains only essential fields for security analysis.' + ), +}); + +export const EVALUATE_ALERT_TOOL_ID = securityTool('evaluate-alert'); + +const EVALUATION_PROMPT = `You are a security analyst evaluating a security alert. Analyze the alert data provided and generate a comprehensive evaluation report. + +SECURITY ALERT DATA: + +{alertData} + +--- + +EVALUATION REQUIREMENTS + +Evaluate the security event described above and provide a structured, markdown-formatted summary suitable for inclusion in an Elastic Security case. Base your analysis solely on the alert data provided above. Your response must include: + +1. 📝 Event Description + - Summarize the event using only the information from the alert data above. + - Include user and host information, risk scores, and severity from the provided context. + - Reference relevant MITRE ATT&CK techniques based on the event details, with hyperlinks to the official MITRE pages. + +2. 🔍 Triage Steps + - List clear, bulleted triage steps tailored to Elastic Security workflows (e.g., alert investigation, timeline creation, entity analytics review). + - Base recommendations on the alert fields provided (e.g., host.name, user.name, source.ip, destination.ip). + - Highlight the detection rule mentioned in the alert data. + +3. 🛡️ Recommended Actions + - Provide prioritized response actions based on the alert data: + - Elastic Defend endpoint response actions (e.g., isolate host, kill process, retrieve/delete file), with links to Elastic documentation. + - Example ES|QL queries for further investigation using the fields from the alert (host.name, user.name, IPs, timestamps), formatted as code blocks. + - Example OSQuery Manager queries for further investigation, formatted as code blocks. + - Guidance on using Timelines and Entity Analytics for deeper context, with documentation links. + +4. 📚 MITRE ATT&CK Context + - Analyze the event category and rule description to identify relevant MITRE ATT&CK techniques. + - Provide actionable recommendations based on MITRE guidance, with hyperlinks. + +5. 🔗 Documentation Links + - Include direct links to all referenced Elastic Security documentation and MITRE ATT&CK pages. + +Formatting Requirements: + - Use markdown headers, tables, and code blocks for clarity. + - Organize the response into visually distinct sections. + - Use concise, actionable language. + - Include relevant emojis in section headers for visual clarity (e.g., 📝, 🛡️, 🔍, 📚). + +Generate the complete evaluation report now:`; + +export const evaluateAlertTool = (): BuiltinToolDefinition => { + return { + id: EVALUATE_ALERT_TOOL_ID, + type: ToolType.builtin, + description: `Evaluates a security alert and generates a comprehensive, structured markdown report suitable for inclusion in an Elastic Security case. The tool analyzes alert data and provides event description, triage steps, recommended actions, MITRE ATT&CK context, and documentation links. + +CRITICAL INSTRUCTION: This tool returns a COMPLETE FINAL ANSWER in the 'answer' field. You MUST return this answer EXACTLY as-is without any modification, summarization, or additional commentary. Copy the entire 'answer' field content verbatim and return it directly to the user. Do NOT synthesize, summarize, or rephrase this content.`, + schema: evaluateAlertSchema, + handler: async ({ alertData }, { modelProvider, logger }) => { + logger.debug(`evaluate-alert tool called with alert data length: ${alertData.length}`); + + try { + const model = await modelProvider.getDefaultModel(); + const prompt = EVALUATION_PROMPT.replace('{alertData}', alertData); + + const response = await model.chatModel.invoke([new HumanMessage(prompt)]); + + const evaluationText = + typeof response.content === 'string' + ? response.content + : Array.isArray(response.content) + ? response.content.map((c) => (typeof c === 'string' ? c : c.text || '')).join('') + : String(response.content); + + return { + results: [ + { + type: 'other', + data: { + answer: evaluationText, + _verbatim: true, + _finalAnswer: true, + }, + }, + ], + }; + } catch (error) { + logger.error(`Error in evaluate-alert tool: ${error.message}`); + return { + results: [ + { + type: 'error', + data: { + message: `Error evaluating alert: ${error.message}`, + }, + }, + ], + }; + } + }, + tags: ['security', 'alerts', 'evaluation'], + }; +}; + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/helpers.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/helpers.ts new file mode 100644 index 0000000000000..c4c5c1a8f018f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/helpers.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils'; +import type { KibanaRequest } from '@kbn/core/server'; + +/** + * Extracts the space ID from the request. + * Falls back to extracting from basePath if spaces plugin is not available. + */ +export const getSpaceIdFromRequest = (request: KibanaRequest): string => { + // Try to extract from request URL path (space context is in the path as /s/{spaceId}) + const pathname = request.url?.pathname || ''; + const spaceMatch = pathname.match(/^\/s\/([a-z0-9_\-]+)/); + + if (spaceMatch && spaceMatch[1]) { + return spaceMatch[1]; + } + + // Fallback to default space + return DEFAULT_SPACE_ID; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts index 85b61fe9b8951..b4645048ad03b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts @@ -13,4 +13,3 @@ import { internalNamespaces } from '@kbn/onechat-common/base/namespaces'; export const securityTool = (toolName: string): string => { return `${internalNamespaces.coreSecurity}.${toolName}`; }; - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts index 29620b59b9c3f..6f476a90c47ae 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts @@ -6,3 +6,4 @@ */ export { alertsTool, SECURITY_ALERTS_TOOL_ID } from './alerts/alerts_tool'; +export { evaluateAlertTool, EVALUATE_ALERT_TOOL_ID } from './alerts/evaluate_alert_tool'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index d6724dc6b1262..342fa9aeed279 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -147,7 +147,7 @@ import type { HealthDiagnosticService } from './lib/telemetry/diagnostic/health_ import { ENTITY_RISK_SCORE_TOOL_ID } from './assistant/tools/entity_risk_score/entity_risk_score'; import type { TelemetryQueryConfiguration } from './lib/telemetry/types'; import { createAlertAttachmentType } from './agent_builder/attachments'; -import { alertsTool } from './agent_builder/tools'; +import { alertsTool, evaluateAlertTool } from './agent_builder/tools'; export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract'; @@ -626,6 +626,7 @@ export class Plugin implements ISecuritySolutionPlugin { } if (plugins.onechat.tools) { plugins.onechat.tools.register(alertsTool()); + plugins.onechat.tools.register(evaluateAlertTool()); } } From 81da0e6c93b289d924c13fe9be8238eea71a86c3 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 18 Nov 2025 16:46:52 -0700 Subject: [PATCH 06/96] evaluate alert tool --- .../server/agent_builder/attachments/alert.ts | 4 +- .../tools/alerts/evaluate_alert_tool.ts | 415 +++++++++++++++++- 2 files changed, 401 insertions(+), 18 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 4678171a40786..d0ab682bd6aeb 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -30,13 +30,13 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition< format: (attachment) => { return { getRepresentation: () => { - return { type: 'text', value: formatAlertData(attachment.data) }; + return { type: 'text', value: attachment.data.alert }; }, }; }, getTools: () => [EVALUATE_ALERT_TOOL_ID], getAgentDescription: () => { - return `Alert attachments contain security alert data. Use the ${EVALUATE_ALERT_TOOL_ID} tool to generate a comprehensive evaluation report. IMPORTANT: When the evaluation tool returns results, return them EXACTLY as provided without summarization or modification.`; + return `Alert attachments contain security alert data. Use the ${EVALUATE_ALERT_TOOL_ID} tool to generate a comprehensive evaluation report with enriched context from related alerts, risk scores, attack discoveries, and Security Labs. IMPORTANT: When the evaluation tool returns results, return them EXACTLY as provided without summarization or modification.`; }, }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts index 33c221c0b63ce..2a23a50848dc7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts @@ -8,20 +8,353 @@ import { z } from '@kbn/zod'; import { ToolType } from '@kbn/onechat-common'; import { HumanMessage } from '@langchain/core/messages'; -import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import type { BuiltinToolDefinition, ScopedModel, ToolEventEmitter } from '@kbn/onechat-server'; +import type { Logger } from '@kbn/logging'; +import type { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; +import { generateEsql } from '@kbn/onechat-genai-utils/tools'; +import { runSearchTool } from '@kbn/onechat-genai-utils/tools/search/run_search_tool'; +import { ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX } from '@kbn/elastic-assistant-common'; +import { DEFAULT_ALERTS_INDEX } from '../../../../common/constants'; +import { getRiskIndex } from '../../../../common/search_strategy/security_solution/risk_score/common'; +import { getSpaceIdFromRequest } from '../helpers'; import { securityTool } from '../constants'; const evaluateAlertSchema = z.object({ alertData: z .string() .describe( - 'The filtered alert data in key-value format (comma-separated, newline-delimited). Contains only essential fields for security analysis.' + 'The filtered alert data in key-value format (comma-separated, newline-delimited). Contains entities like host.name, user.name, source.ip, destination.ip, file.hash.sha256, file.name, file.path, service.name, kibana.alert.uuid, etc.' ), }); export const EVALUATE_ALERT_TOOL_ID = securityTool('evaluate-alert'); -const EVALUATION_PROMPT = `You are a security analyst evaluating a security alert. Analyze the alert data provided and generate a comprehensive evaluation report. +// Essential fields to keep when querying alerts to minimize token usage +const KEEP_FIELDS = [ + '_id', + '@timestamp', + 'host.name', + 'user.name', + 'kibana.alert.rule.name', + 'kibana.alert.severity', + 'kibana.alert.risk_score', + 'source.ip', + 'destination.ip', + 'event.category', + 'message', +].join(', '); + +const ENTITY_EXTRACTION_PROMPT = `Extract security entities from the following alert data. Return a JSON object with the following structure: +{ + "alertId": "string or null", + "hostNames": ["string"], + "userNames": ["string"], + "sourceIps": ["string"], + "destinationIps": ["string"], + "fileHashes": ["string"], + "fileNames": ["string"], + "filePaths": ["string"], + "serviceNames": ["string"], + "mitreTechniques": ["string"], + "ruleName": "string or null" +} + +Extract all values found in the alert data. If a field is not present, use an empty array [] or null. +Only include non-null, non-empty values. + +Alert data: +{alertData} + +Return only valid JSON, no other text:`; + +/** + * Uses LLM to extract entities from alert data + */ +const extractEntitiesWithLLM = async ( + alertData: string, + model: ScopedModel, + logger: Logger +): Promise<{ + alertId?: string; + hostNames: string[]; + userNames: string[]; + sourceIps: string[]; + destinationIps: string[]; + fileHashes: string[]; + fileNames: string[]; + filePaths: string[]; + serviceNames: string[]; + mitreTechniques: string[]; + ruleName?: string; +}> => { + try { + const prompt = ENTITY_EXTRACTION_PROMPT.replace('{alertData}', alertData); + const response = await model.chatModel.invoke([new HumanMessage(prompt)]); + + const responseText = + typeof response.content === 'string' + ? response.content + : Array.isArray(response.content) + ? response.content.map((c) => (typeof c === 'string' ? c : c.text || '')).join('') + : String(response.content); + + // Extract JSON from response (in case LLM adds extra text) + const jsonMatch = responseText.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('No JSON found in LLM response'); + } + + const entities = JSON.parse(jsonMatch[0]) as { + alertId?: string | null; + hostNames?: string[]; + userNames?: string[]; + sourceIps?: string[]; + destinationIps?: string[]; + fileHashes?: string[]; + fileNames?: string[]; + filePaths?: string[]; + serviceNames?: string[]; + mitreTechniques?: string[]; + ruleName?: string | null; + }; + logger.debug(`Extracted entities: ${JSON.stringify(entities, null, 2)}`); + + return { + alertId: entities.alertId || undefined, + hostNames: entities.hostNames || [], + userNames: entities.userNames || [], + sourceIps: entities.sourceIps || [], + destinationIps: entities.destinationIps || [], + fileHashes: entities.fileHashes || [], + fileNames: entities.fileNames || [], + filePaths: entities.filePaths || [], + serviceNames: entities.serviceNames || [], + mitreTechniques: entities.mitreTechniques || [], + ruleName: entities.ruleName || undefined, + }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.warn(`Error extracting entities with LLM: ${errorMessage}`); + // Return empty entities on error + return { + hostNames: [], + userNames: [], + sourceIps: [], + destinationIps: [], + fileHashes: [], + fileNames: [], + filePaths: [], + serviceNames: [], + mitreTechniques: [], + }; + } +}; + +/** + * Queries related alerts using generateEsql tool + */ +const queryRelatedAlerts = async ( + entities: { + hostNames: string[]; + userNames: string[]; + sourceIps: string[]; + destinationIps: string[]; + fileHashes: string[]; + }, + spaceId: string, + model: ScopedModel, + esClient: IScopedClusterClient, + logger: Logger, + events: ToolEventEmitter +): Promise => { + try { + const conditions: string[] = []; + + if (entities.hostNames.length > 0) { + conditions.push(`host.name is "${entities.hostNames[0]}"`); + } + if (entities.userNames.length > 0) { + conditions.push(`user.name is "${entities.userNames[0]}"`); + } + if (entities.sourceIps.length > 0) { + conditions.push(`source.ip is "${entities.sourceIps[0]}"`); + } + if (entities.destinationIps.length > 0) { + conditions.push(`destination.ip is "${entities.destinationIps[0]}"`); + } + if (entities.fileHashes.length > 0) { + conditions.push(`file.hash.sha256 is "${entities.fileHashes[0]}"`); + } + + if (conditions.length === 0) { + return 'No related alerts found (no entities to search for).'; + } + + const index = `${DEFAULT_ALERTS_INDEX}-${spaceId}`; + const nlQuery = `Find related security alerts from the last 7 days where ${conditions.join( + ' OR ' + )}. Include only essential fields: ${KEEP_FIELDS}. Limit to 50 results.`; + + const additionalInstructions = `When querying security alert indices, ALWAYS use the KEEP command to filter fields and reduce response size. Include these essential fields: ${KEEP_FIELDS}. Example: FROM ${index} | KEEP ${KEEP_FIELDS} | ...`; + + logger.debug(`Querying related alerts with natural language: ${nlQuery}`); + + const esqlResponse = await generateEsql({ + nlQuery, + index, + additionalInstructions, + executeQuery: true, + model, + esClient: esClient.asCurrentUser, + logger, + events, + }); + + if (esqlResponse.error) { + return `Error querying related alerts: ${esqlResponse.error}`; + } + + if (esqlResponse.results && esqlResponse.results.values.length > 0) { + const count = esqlResponse.results.values.length; + return `Found ${count} related alerts. Query: ${ + esqlResponse.query || 'N/A' + }. Results summary: ${esqlResponse.answer || 'See query results.'}`; + } + + return 'No related alerts found.'; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.warn(`Error querying related alerts: ${errorMessage}`); + return `Error querying related alerts: ${errorMessage}`; + } +}; + +/** + * Queries risk scores using search tool + */ +const queryRiskScores = async ( + entities: { + hostNames: string[]; + userNames: string[]; + }, + spaceId: string, + model: ScopedModel, + esClient: IScopedClusterClient, + logger: Logger, + events: ToolEventEmitter +): Promise => { + try { + const riskInfo: string[] = []; + const riskIndex = getRiskIndex(spaceId, true); + + if (entities.hostNames.length > 0) { + const nlQuery = `Find risk scores for hosts: ${entities.hostNames.join( + ', ' + )}. Include host.name, host.risk.calculated_score_norm, and host.risk.calculated_level fields.`; + const results = await runSearchTool({ + nlQuery, + index: riskIndex, + model, + esClient: esClient.asCurrentUser, + logger, + events, + }); + + if (results.length > 0 && !results.some((r) => r.type === 'error')) { + riskInfo.push( + `Host Risk Scores: Found risk information for ${entities.hostNames.length} host(s).` + ); + } + } + + if (entities.userNames.length > 0) { + const nlQuery = `Find risk scores for users: ${entities.userNames.join( + ', ' + )}. Include user.name, user.risk.calculated_score_norm, and user.risk.calculated_level fields.`; + const results = await runSearchTool({ + nlQuery, + index: riskIndex, + model, + esClient: esClient.asCurrentUser, + logger, + events, + }); + + if (results.length > 0 && !results.some((r) => r.type === 'error')) { + riskInfo.push( + `User Risk Scores: Found risk information for ${entities.userNames.length} user(s).` + ); + } + } + + return riskInfo.length > 0 ? riskInfo.join('\n\n') : 'No risk score information found.'; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.warn(`Error querying risk scores: ${errorMessage}`); + return `Error querying risk scores: ${errorMessage}`; + } +}; + +/** + * Queries attack discoveries using search tool + */ +const queryAttackDiscoveries = async ( + alertId: string | undefined, + spaceId: string, + model: ScopedModel, + esClient: IScopedClusterClient, + logger: Logger, + events: ToolEventEmitter +): Promise => { + if (!alertId) { + return 'No alert ID available to query attack discoveries.'; + } + + try { + // Query both scheduled and ad-hoc attack discovery indices + const indexPattern = [ + `${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}*`, + `.ds-.adhoc${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}*`, + `.internal${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}*`, + `.internal.ds-.adhoc${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}*`, + ].join(','); + + const nlQuery = `Find attack discoveries that include alert ID "${alertId}" in the kibana.alert.attack_discovery.alert_ids field. Include kibana.alert.attack_discovery.title, kibana.alert.attack_discovery.summary_markdown, and kibana.alert.attack_discovery.alert_ids fields. Limit to 5 results.`; + + const results = await runSearchTool({ + nlQuery, + index: indexPattern, + model, + esClient: esClient.asCurrentUser, + logger, + events, + }); + + if (results.length > 0 && !results.some((r) => r.type === 'error')) { + return `This alert is part of one or more attack discoveries. Found attack discovery information.`; + } + + return 'This alert is not part of any known attack discovery.'; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.warn(`Error querying attack discoveries: ${errorMessage}`); + return `Error querying attack discoveries: ${errorMessage}`; + } +}; + +/** + * Queries Security Labs knowledge base for relevant information + * Note: kbDataClient is not available in tool handler context, so this is a placeholder + * that can be enhanced if knowledge base access is added to the context in the future + */ +const querySecurityLabs = async (entities: unknown, logger: Logger): Promise => { + // Security Labs knowledge base querying is not currently available in the tool handler context + // This can be enhanced in the future if kbDataClient is added to ToolHandlerContext + logger.debug('Security Labs knowledge base querying not available in tool handler context'); + return 'Security Labs knowledge base querying is not currently available. Consider using the Security Labs tool separately if needed.'; +}; + +const EVALUATION_PROMPT = `You are a security analyst evaluating a security alert with enriched context. Analyze the alert data and all provided context to generate a comprehensive evaluation report. SECURITY ALERT DATA: @@ -29,33 +362,54 @@ SECURITY ALERT DATA: --- +ENRICHED CONTEXT: + +RELATED ALERTS: +{relatedAlerts} + +RISK SCORES: +{riskScores} + +ATTACK DISCOVERIES: +{attackDiscoveries} + +SECURITY LABS: +{securityLabs} + +--- + EVALUATION REQUIREMENTS -Evaluate the security event described above and provide a structured, markdown-formatted summary suitable for inclusion in an Elastic Security case. Base your analysis solely on the alert data provided above. Your response must include: +Evaluate the security event described above and provide a structured, markdown-formatted summary suitable for inclusion in an Elastic Security case. Use the enriched context above to provide a comprehensive analysis. Your response must include: 1. 📝 Event Description - - Summarize the event using only the information from the alert data above. + - Summarize the event using information from the alert data and enriched context. - Include user and host information, risk scores, and severity from the provided context. - Reference relevant MITRE ATT&CK techniques based on the event details, with hyperlinks to the official MITRE pages. + - If this alert is part of an attack discovery, highlight that context. 2. 🔍 Triage Steps - List clear, bulleted triage steps tailored to Elastic Security workflows (e.g., alert investigation, timeline creation, entity analytics review). - - Base recommendations on the alert fields provided (e.g., host.name, user.name, source.ip, destination.ip). + - Base recommendations on the alert fields and related alerts found (e.g., host.name, user.name, source.ip, destination.ip). - Highlight the detection rule mentioned in the alert data. + - If related alerts were found, mention patterns or trends observed. 3. 🛡️ Recommended Actions - - Provide prioritized response actions based on the alert data: + - Provide prioritized response actions based on the alert data and context: - Elastic Defend endpoint response actions (e.g., isolate host, kill process, retrieve/delete file), with links to Elastic documentation. - Example ES|QL queries for further investigation using the fields from the alert (host.name, user.name, IPs, timestamps), formatted as code blocks. - Example OSQuery Manager queries for further investigation, formatted as code blocks. - Guidance on using Timelines and Entity Analytics for deeper context, with documentation links. + - If risk scores indicate high-risk entities, recommend immediate investigation. 4. 📚 MITRE ATT&CK Context - Analyze the event category and rule description to identify relevant MITRE ATT&CK techniques. - Provide actionable recommendations based on MITRE guidance, with hyperlinks. + - If Security Labs articles were found, reference relevant research and findings. 5. 🔗 Documentation Links - Include direct links to all referenced Elastic Security documentation and MITRE ATT&CK pages. + - If Security Labs articles were found, include links to those articles. Formatting Requirements: - Use markdown headers, tables, and code blocks for clarity. @@ -69,16 +423,45 @@ export const evaluateAlertTool = (): BuiltinToolDefinition { + handler: async ({ alertData }, { request, esClient, modelProvider, logger, events }) => { logger.debug(`evaluate-alert tool called with alert data length: ${alertData.length}`); try { + const spaceId = getSpaceIdFromRequest(request); const model = await modelProvider.getDefaultModel(); - const prompt = EVALUATION_PROMPT.replace('{alertData}', alertData); + + // Extract entities using LLM + const entities = await extractEntitiesWithLLM(alertData, model, logger); + logger.debug(`Extracted entities: ${JSON.stringify(entities, null, 2)}`); + + // Query related alerts, risk scores, attack discoveries, and security labs in parallel + const [relatedAlerts, riskScores, attackDiscoveries, securityLabs] = await Promise.all([ + queryRelatedAlerts(entities, spaceId, model, esClient, logger, events), + queryRiskScores(entities, spaceId, model, esClient, logger, events), + queryAttackDiscoveries(entities.alertId, spaceId, model, esClient, logger, events), + querySecurityLabs(entities, logger), + ]); + + // Build enriched context + const enrichedContext = { + relatedAlerts, + riskScores, + attackDiscoveries, + securityLabs, + }; + + logger.debug(`Enriched context gathered: ${JSON.stringify(enrichedContext, null, 2)}`); + + // Generate evaluation using LLM + const prompt = EVALUATION_PROMPT.replace('{alertData}', alertData) + .replace('{relatedAlerts}', relatedAlerts) + .replace('{riskScores}', riskScores) + .replace('{attackDiscoveries}', attackDiscoveries) + .replace('{securityLabs}', securityLabs); const response = await model.chatModel.invoke([new HumanMessage(prompt)]); @@ -86,8 +469,8 @@ CRITICAL INSTRUCTION: This tool returns a COMPLETE FINAL ANSWER in the 'answer' typeof response.content === 'string' ? response.content : Array.isArray(response.content) - ? response.content.map((c) => (typeof c === 'string' ? c : c.text || '')).join('') - : String(response.content); + ? response.content.map((c) => (typeof c === 'string' ? c : c.text || '')).join('') + : String(response.content); return { results: [ @@ -101,14 +484,15 @@ CRITICAL INSTRUCTION: This tool returns a COMPLETE FINAL ANSWER in the 'answer' }, ], }; - } catch (error) { - logger.error(`Error in evaluate-alert tool: ${error.message}`); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Error in evaluate-alert tool: ${errorMessage}`); return { results: [ { type: 'error', data: { - message: `Error evaluating alert: ${error.message}`, + message: `Error evaluating alert: ${errorMessage}`, }, }, ], @@ -118,4 +502,3 @@ CRITICAL INSTRUCTION: This tool returns a COMPLETE FINAL ANSWER in the 'answer' tags: ['security', 'alerts', 'evaluation'], }; }; - From 801522b6e3a2b2ce80c972f7668b0e9941137131 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 18 Nov 2025 17:54:24 -0700 Subject: [PATCH 07/96] Alert attachments specific workflow --- .../server/agent_builder/attachments/alert.ts | 98 ++++++++----------- 1 file changed, 39 insertions(+), 59 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index d0ab682bd6aeb..8f1b973a1b9a9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -8,7 +8,7 @@ import type { AlertAttachmentData } from '@kbn/onechat-common/attachments'; import { AttachmentType, alertAttachmentDataSchema } from '@kbn/onechat-common/attachments'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; -import { EVALUATE_ALERT_TOOL_ID } from '../tools'; +import { platformCoreTools } from '@kbn/onechat-common/tools'; /** * Creates the definition for the `alert` attachment type. @@ -30,78 +30,58 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition< format: (attachment) => { return { getRepresentation: () => { - return { type: 'text', value: attachment.data.alert }; + return { type: 'text', value: formatAlertData(attachment.data) }; }, }; }, - getTools: () => [EVALUATE_ALERT_TOOL_ID], - getAgentDescription: () => { - return `Alert attachments contain security alert data. Use the ${EVALUATE_ALERT_TOOL_ID} tool to generate a comprehensive evaluation report with enriched context from related alerts, risk scores, attack discoveries, and Security Labs. IMPORTANT: When the evaluation tool returns results, return them EXACTLY as provided without summarization or modification.`; + getTools: () => { + const tools = [ + platformCoreTools.generateEsql, + platformCoreTools.executeEsql, + platformCoreTools.search, + ]; + return tools; }, - }; -}; + getAgentDescription: () => { + const description = `You have access to security alert data. To provide a comprehensive analysis, you MUST gather enriched context by querying for related information. -/** - * Essential fields to include in alert attachments to reduce token usage. - * These fields provide the most important context for security analysis. - */ -const ESSENTIAL_ALERT_FIELDS = new Set([ - '@timestamp', - 'kibana.alert.original_time', - 'kibana.alert.rule.name', - 'kibana.alert.rule.description', - 'kibana.alert.severity', - 'kibana.alert.risk_score', - 'kibana.alert.workflow_status', - 'host.name', - 'user.name', - 'source.ip', - 'destination.ip', - 'event.category', - 'event.action', - 'message', - '_id', -]); +SECURITY ALERT DATA: +{alertData} -/** - * Filters alert data to include only essential fields to reduce token usage. - * - * @param alertData - The raw alert data string (comma-separated key-value pairs, newline-delimited) - * @returns Filtered alert data string with only essential fields - */ -const filterEssentialFields = (alertData: string): string => { - const lines = alertData.split('\n').filter((line) => line.trim().length > 0); - const filteredLines: string[] = []; +--- - for (const line of lines) { - const commaIndex = line.indexOf(','); - if (commaIndex > 0) { - const key = line.substring(0, commaIndex).trim(); - // Include essential fields and any field that starts with kibana.alert (for completeness) - if (ESSENTIAL_ALERT_FIELDS.has(key) || key.startsWith('kibana.alert.')) { - filteredLines.push(line); - } - } - } +MANDATORY WORKFLOW - Complete in order: + +1. Extract entities: host.name, user.name, source.ip, destination.ip, file.hash.sha256, kibana.alert.uuid (or _id), kibana.alert.rule.name, kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, event.category, event.action - return filteredLines.join('\n'); +2. Query RELATED ALERTS: + Tool: ${platformCoreTools.search} + Parameters: { query: "Find security alerts from last 7 days where host.name is '[host]' OR user.name is '[user]' OR source.ip is '[ip]' OR destination.ip is '[dest_ip]'", index: ".alerts-security.alerts-*" } + +3. Query RISK SCORES: + Tool: ${platformCoreTools.search} + Parameters: { query: "Find risk scores for host.name '[host]' OR user.name '[user]'", index: "risk-score.risk-score-latest-default" } + +4. Query ATTACK DISCOVERIES: + Tool: ${platformCoreTools.search} + Parameters: { query: "Find attack discoveries where kibana.alert.attack_discovery.alert_ids contains '[alert ID]'", index: ".alerts-security.alerts-attack.discovery-*,.adhoc.alerts-security.alerts-attack.discovery-*" } + +5. Query SECURITY LABS: + Tool: ${platformCoreTools.search} + Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]", index: ".kibana-elastic-ai-assistant-knowledge-base-*" } + +CRITICAL: You MUST call all 4 tools (steps 2-5) before responding. Do not skip any step.`; + return description; + }, + }; }; /** * Formats alert data for display. - * The alert data is filtered to include only essential fields to reduce token usage. * * @param data - The alert attachment data containing the alert string - * @returns Formatted string representation of the filtered alert data + * @returns Formatted string representation of the alert data */ const formatAlertData = (data: AlertAttachmentData): string => { - const filteredAlert = filterEssentialFields(data.alert); - - return `SECURITY ALERT DATA - -${filteredAlert} - ---- - -To evaluate this alert, use the ${EVALUATE_ALERT_TOOL_ID} tool with the alertData parameter set to the filtered alert data shown above.`; + return data.alert; }; From fbcd6db35e3b8a02ee7edbf043e3ca0874f416dd Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 18 Nov 2025 18:29:39 -0700 Subject: [PATCH 08/96] security specific tools --- .../onechat/onechat-server/allow_lists.ts | 4 + .../server/agent_builder/attachments/alert.ts | 30 ++-- .../tools/alerts/alerts_index_search_tool.ts | 132 +++++++++++++++++ .../attack_discovery_search_tool.ts | 134 ++++++++++++++++++ .../server/agent_builder/tools/index.ts | 16 +++ .../risk_score/risk_score_search_tool.ts | 67 +++++++++ .../security_labs_search_tool.ts | 69 +++++++++ .../security_solution/server/plugin.ts | 13 +- 8 files changed, 452 insertions(+), 13 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery/attack_discovery_search_tool.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/risk_score/risk_score_search_tool.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs/security_labs_search_tool.ts diff --git a/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts b/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts index 2e6e55b9ef429..7c41e334f1de2 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts @@ -22,7 +22,11 @@ export const AGENT_BUILDER_BUILTIN_TOOLS: string[] = [ 'observability.get_downstream_dependencies', // Security Solution 'core.security.alerts', + 'core.security.alerts-index-search', 'core.security.evaluate-alert', + 'core.security.risk-score-search', + 'core.security.attack-discovery-search', + 'core.security.security-labs-search', ]; /** diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 8f1b973a1b9a9..da570d3508361 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -8,7 +8,12 @@ import type { AlertAttachmentData } from '@kbn/onechat-common/attachments'; import { AttachmentType, alertAttachmentDataSchema } from '@kbn/onechat-common/attachments'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; -import { platformCoreTools } from '@kbn/onechat-common/tools'; +import { + SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID, + SECURITY_RISK_SCORE_SEARCH_TOOL_ID, + SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, + SECURITY_LABS_SEARCH_TOOL_ID, +} from '../tools'; /** * Creates the definition for the `alert` attachment type. @@ -36,9 +41,10 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition< }, getTools: () => { const tools = [ - platformCoreTools.generateEsql, - platformCoreTools.executeEsql, - platformCoreTools.search, + SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID, + SECURITY_RISK_SCORE_SEARCH_TOOL_ID, + SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, + SECURITY_LABS_SEARCH_TOOL_ID, ]; return tools; }, @@ -55,20 +61,20 @@ MANDATORY WORKFLOW - Complete in order: 1. Extract entities: host.name, user.name, source.ip, destination.ip, file.hash.sha256, kibana.alert.uuid (or _id), kibana.alert.rule.name, kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, event.category, event.action 2. Query RELATED ALERTS: - Tool: ${platformCoreTools.search} - Parameters: { query: "Find security alerts from last 7 days where host.name is '[host]' OR user.name is '[user]' OR source.ip is '[ip]' OR destination.ip is '[dest_ip]'", index: ".alerts-security.alerts-*" } + Tool: ${SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID} + Parameters: { query: "Find security alerts from last 7 days where host.name is '[host]' OR user.name is '[user]' OR source.ip is '[ip]' OR destination.ip is '[dest_ip]'" } 3. Query RISK SCORES: - Tool: ${platformCoreTools.search} - Parameters: { query: "Find risk scores for host.name '[host]' OR user.name '[user]'", index: "risk-score.risk-score-latest-default" } + Tool: ${SECURITY_RISK_SCORE_SEARCH_TOOL_ID} + Parameters: { query: "Find risk scores for host.name '[host]' OR user.name '[user]'. Include host.risk.calculated_score_norm, host.risk.calculated_level, user.risk.calculated_score_norm, and user.risk.calculated_level fields." } 4. Query ATTACK DISCOVERIES: - Tool: ${platformCoreTools.search} - Parameters: { query: "Find attack discoveries where kibana.alert.attack_discovery.alert_ids contains '[alert ID]'", index: ".alerts-security.alerts-attack.discovery-*,.adhoc.alerts-security.alerts-attack.discovery-*" } + Tool: ${SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID} + Parameters: { query: "Find attack discoveries where kibana.alert.attack_discovery.alert_ids contains '[alert ID]'. Include kibana.alert.attack_discovery.title, kibana.alert.attack_discovery.summary_markdown, and kibana.alert.attack_discovery.alert_ids fields." } 5. Query SECURITY LABS: - Tool: ${platformCoreTools.search} - Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]", index: ".kibana-elastic-ai-assistant-knowledge-base-*" } + Tool: ${SECURITY_LABS_SEARCH_TOOL_ID} + Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]" } CRITICAL: You MUST call all 4 tools (steps 2-5) before responding. Do not skip any step.`; return description; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts new file mode 100644 index 0000000000000..220991782b897 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts @@ -0,0 +1,132 @@ +/* + * 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. + */ + +import { z } from '@kbn/zod'; +import { ToolType } from '@kbn/onechat-common'; +import { generateEsql } from '@kbn/onechat-genai-utils/tools'; +import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import { DEFAULT_ALERTS_INDEX } from '../../../../common/constants'; +import { getSpaceIdFromRequest } from '../helpers'; +import { securityTool } from '../constants'; + +const alertsIndexSearchSchema = z.object({ + query: z + .string() + .describe( + 'A natural language query expressing the search request for security alerts. Use this to find related alerts by entities like host.name, user.name, source.ip, destination.ip, file.hash.sha256, etc.' + ), +}); + +export const SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID = securityTool('alerts-index-search'); + +const KEEP_FIELDS = [ + '@timestamp', + 'host.name', + 'user.name', + 'kibana.alert.rule.name', + 'kibana.alert.severity', + 'kibana.alert.risk_score', + 'source.ip', + 'destination.ip', + 'event.category', + 'message', +].join(', '); + +const ADDITIONAL_INSTRUCTIONS = `When querying security alert indices, ALWAYS use the KEEP command to filter fields and reduce response size. Include these essential fields: ${KEEP_FIELDS}. Limit results to 50 alerts. Example: FROM .alerts-security.alerts-* | KEEP ${KEEP_FIELDS} | ...`; + +export const alertsIndexSearchTool = (): BuiltinToolDefinition => { + return { + id: SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID, + type: ToolType.builtin, + description: `Search and analyze security alerts from the alerts index. Use this tool to find related alerts by entities like host names, user names, IP addresses, file hashes, or other alert fields. Automatically filters to essential fields and limits results to 50 alerts.`, + schema: alertsIndexSearchSchema, + handler: async ( + { query: nlQuery }, + { request, esClient, modelProvider, logger, events } + ) => { + const spaceId = getSpaceIdFromRequest(request); + const searchIndex = `${DEFAULT_ALERTS_INDEX}-${spaceId}`; + + logger.debug(`alerts-index-search tool called with query: ${nlQuery}, index: ${searchIndex}`); + + try { + // Generate ES|QL query with automatic KEEP field filtering + const esqlResponse = await generateEsql({ + nlQuery, + index: searchIndex, + additionalInstructions: ADDITIONAL_INSTRUCTIONS, + executeQuery: true, + model: await modelProvider.getDefaultModel(), + esClient: esClient.asCurrentUser, + logger, + events, + }); + + if (esqlResponse.error) { + return { + results: [ + { + type: 'error', + data: { + message: esqlResponse.error, + }, + }, + ], + }; + } + + const results = []; + + if (esqlResponse.query) { + results.push({ + type: 'query', + data: { + esql: esqlResponse.query, + }, + }); + } + + if (esqlResponse.results) { + results.push({ + type: 'tabularData', + data: { + source: 'esql', + query: esqlResponse.query || '', + columns: esqlResponse.results.columns, + values: esqlResponse.results.values, + }, + }); + } + + if (esqlResponse.answer) { + results.push({ + type: 'other', + data: { + answer: esqlResponse.answer, + }, + }); + } + + return { results }; + } catch (error) { + logger.error(`Error in alerts-index-search tool: ${error.message}`); + return { + results: [ + { + type: 'error', + data: { + message: `Error: ${error.message}`, + }, + }, + ], + }; + } + }, + tags: ['security', 'alerts', 'search'], + }; +}; + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery/attack_discovery_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery/attack_discovery_search_tool.ts new file mode 100644 index 0000000000000..2ebbb6801d7b5 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery/attack_discovery_search_tool.ts @@ -0,0 +1,134 @@ +/* + * 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. + */ + +import { z } from '@kbn/zod'; +import { ToolType } from '@kbn/onechat-common'; +import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import { executeEsql } from '@kbn/onechat-genai-utils/tools/utils/esql'; +import { ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX } from '@kbn/elastic-assistant-common'; +import { getSpaceIdFromRequest } from '../helpers'; +import { securityTool } from '../constants'; + +const attackDiscoverySearchSchema = z.object({ + query: z + .string() + .describe( + 'A natural language query expressing the search request for attack discoveries. Use this to find attack discoveries that include specific alert IDs in the kibana.alert.attack_discovery.alert_ids field. Include fields like kibana.alert.attack_discovery.title, kibana.alert.attack_discovery.summary_markdown, and kibana.alert.attack_discovery.alert_ids.' + ), +}); + +export const SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID = securityTool('attack-discovery-search'); + +/** + * Extracts alert IDs from a natural language query. + * Looks for patterns like "alert ID 'xxx'", "alert ID xxx", or quoted UUIDs. + */ +const extractAlertIds = (query: string): string[] => { + const alertIds: string[] = []; + + // Pattern 1: "alert ID 'xxx'" or "alert ID \"xxx\"" + const quotedPattern = /alert\s+id[:\s]+['"]([^'"]+)['"]/gi; + let match; + while ((match = quotedPattern.exec(query)) !== null) { + alertIds.push(match[1]); + } + + // Pattern 2: UUID pattern (8-4-4-4-12 hex digits) + const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi; + const uuidMatches = query.match(uuidPattern); + if (uuidMatches) { + alertIds.push(...uuidMatches); + } + + return [...new Set(alertIds)]; // Remove duplicates +}; + +export const attackDiscoverySearchTool = (): BuiltinToolDefinition< + typeof attackDiscoverySearchSchema +> => { + return { + id: SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, + type: ToolType.builtin, + description: `Search and analyze attack discoveries. Use this tool to find attack discoveries related to specific alerts by searching for alert IDs in the kibana.alert.attack_discovery.alert_ids field. Automatically queries both scheduled and ad-hoc attack discovery indices for the current space. Limits results to 5 attack discoveries.`, + schema: attackDiscoverySearchSchema, + handler: async ({ query: nlQuery }, { request, esClient, logger }) => { + const spaceId = getSpaceIdFromRequest(request); + + logger.debug(`attack-discovery-search tool called with query: ${nlQuery}`); + + try { + // Extract alert IDs from the natural language query + const alertIds = extractAlertIds(nlQuery); + + // Build date filter for last 7 days + const now = new Date(); + const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const dateFilter = `@timestamp >= "${sevenDaysAgo.toISOString()}" AND @timestamp <= "${now.toISOString()}"`; + + // Build WHERE clause + let whereClause = dateFilter; + if (alertIds.length > 0) { + // Search for alert IDs in the array field + const alertIdConditions = alertIds + .map((id) => `"${id}" IN kibana.alert.attack_discovery.alert_ids`) + .join(' OR '); + whereClause = `${dateFilter} AND (${alertIdConditions})`; + } + + // Build ES|QL query + const esqlQuery = [ + `FROM ${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}*,.adhoc.${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}* METADATA _id`, + `| WHERE ${whereClause}`, + `| KEEP _id, kibana.alert.attack_discovery.title, kibana.alert.attack_discovery.summary_markdown, kibana.alert.workflow_status, kibana.alert.attack_discovery.alert_ids, kibana.alert.case_ids, @timestamp`, + `| SORT @timestamp DESC`, + `| LIMIT 5`, + ].join('\n'); + + logger.debug(`Executing ES|QL query: ${esqlQuery}`); + + const esqlResponse = await executeEsql({ + query: esqlQuery, + esClient: esClient.asCurrentUser, + }); + console.log('ATT ==>', esqlResponse); + + const results = [ + { + type: 'query' as const, + data: { + esql: esqlQuery, + }, + }, + { + type: 'tabularData' as const, + data: { + source: 'esql', + query: esqlQuery, + columns: esqlResponse.columns, + values: esqlResponse.values, + }, + }, + ]; + + return { results }; + } catch (error) { + logger.error(`Error in attack-discovery-search tool: ${error.message}`); + return { + results: [ + { + type: 'error', + data: { + message: `Error: ${error.message}`, + }, + }, + ], + }; + } + }, + tags: ['security', 'attack-discovery', 'search'], + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts index 6f476a90c47ae..074c33cb4dded 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts @@ -6,4 +6,20 @@ */ export { alertsTool, SECURITY_ALERTS_TOOL_ID } from './alerts/alerts_tool'; +export { + alertsIndexSearchTool, + SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID, +} from './alerts/alerts_index_search_tool'; export { evaluateAlertTool, EVALUATE_ALERT_TOOL_ID } from './alerts/evaluate_alert_tool'; +export { + riskScoreSearchTool, + SECURITY_RISK_SCORE_SEARCH_TOOL_ID, +} from './risk_score/risk_score_search_tool'; +export { + attackDiscoverySearchTool, + SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, +} from './attack_discovery/attack_discovery_search_tool'; +export { + securityLabsSearchTool, + SECURITY_LABS_SEARCH_TOOL_ID, +} from './security_labs/security_labs_search_tool'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/risk_score/risk_score_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/risk_score/risk_score_search_tool.ts new file mode 100644 index 0000000000000..dbdea3cc9099c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/risk_score/risk_score_search_tool.ts @@ -0,0 +1,67 @@ +/* + * 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. + */ + +import { z } from '@kbn/zod'; +import { ToolType } from '@kbn/onechat-common'; +import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import { runSearchTool } from '@kbn/onechat-genai-utils/tools/search/run_search_tool'; +import { getRiskIndex } from '../../../../common/search_strategy/security_solution/risk_score/common'; +import { getSpaceIdFromRequest } from '../helpers'; +import { securityTool } from '../constants'; + +const riskScoreSearchSchema = z.object({ + query: z + .string() + .describe( + 'A natural language query expressing the search request for risk scores. Use this to find risk scores for hosts (host.name) or users (user.name). Include fields like host.risk.calculated_score_norm, host.risk.calculated_level, user.risk.calculated_score_norm, user.risk.calculated_level.' + ), +}); + +export const SECURITY_RISK_SCORE_SEARCH_TOOL_ID = securityTool('risk-score-search'); + +export const riskScoreSearchTool = (): BuiltinToolDefinition => { + return { + id: SECURITY_RISK_SCORE_SEARCH_TOOL_ID, + type: ToolType.builtin, + description: `Search and analyze risk scores for hosts and users. Use this tool to find risk score information including calculated_score_norm and calculated_level for entities. Automatically queries the latest risk score index for the current space.`, + schema: riskScoreSearchSchema, + handler: async ({ query: nlQuery }, { request, esClient, modelProvider, logger, events }) => { + const spaceId = getSpaceIdFromRequest(request); + const riskIndex = getRiskIndex(spaceId, true); + + logger.debug(`risk-score-search tool called with query: ${nlQuery}, index: ${riskIndex}`); + + try { + const results = await runSearchTool({ + nlQuery, + index: riskIndex, + model: await modelProvider.getDefaultModel(), + esClient: esClient.asCurrentUser, + logger, + events, + }); + + console.log('RISK ==>', results); + + return { results }; + } catch (error) { + logger.error(`Error in risk-score-search tool: ${error.message}`); + return { + results: [ + { + type: 'error', + data: { + message: `Error: ${error.message}`, + }, + }, + ], + }; + } + }, + tags: ['security', 'risk-score', 'search'], + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs/security_labs_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs/security_labs_search_tool.ts new file mode 100644 index 0000000000000..fbfac3b64b315 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs/security_labs_search_tool.ts @@ -0,0 +1,69 @@ +/* + * 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. + */ + +import { z } from '@kbn/zod'; +import { ToolType } from '@kbn/onechat-common'; +import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import { runSearchTool } from '@kbn/onechat-genai-utils/tools/search/run_search_tool'; +import { SECURITY_LABS_RESOURCE } from '@kbn/elastic-assistant-plugin/server/routes/knowledge_base/constants'; +import { securityTool } from '../constants'; + +const securityLabsSearchSchema = z.object({ + query: z + .string() + .describe( + 'A natural language query expressing the search request for Security Labs articles. Use this to find Security Labs content about specific malware, attack techniques, MITRE ATT&CK techniques, or rule names.' + ), +}); + +export const SECURITY_LABS_SEARCH_TOOL_ID = securityTool('security-labs-search'); + +const SECURITY_LABS_INDEX_PATTERN = '.kibana-elastic-ai-assistant-knowledge-base-default'; + +export const securityLabsSearchTool = (): BuiltinToolDefinition< + typeof securityLabsSearchSchema +> => { + return { + id: SECURITY_LABS_SEARCH_TOOL_ID, + type: ToolType.builtin, + description: `Search and analyze Security Labs knowledge base content. Use this tool to find Security Labs articles about specific malware, attack techniques, MITRE ATT&CK techniques, or rule names. Automatically filters to Security Labs content only and limits results to 10 articles.`, + schema: securityLabsSearchSchema, + handler: async ({ query: nlQuery }, { esClient, modelProvider, logger, events }) => { + logger.debug(`security-labs-search tool called with query: ${nlQuery}`); + + try { + // Enhance query to filter by Security Labs resource and limit results + const enhancedQuery = `${nlQuery} Filter to only Security Labs content (kb_resource: ${SECURITY_LABS_RESOURCE}). Limit to 3 results.`; + + const results = await runSearchTool({ + nlQuery: enhancedQuery, + index: SECURITY_LABS_INDEX_PATTERN, + model: await modelProvider.getDefaultModel(), + esClient: esClient.asCurrentUser, + logger, + events, + }); + console.log('SL ==>', results); + + return { results }; + } catch (error) { + logger.error(`Error in security-labs-search tool: ${error.message}`); + return { + results: [ + { + type: 'error', + data: { + message: `Error: ${error.message}`, + }, + }, + ], + }; + } + }, + tags: ['security', 'security-labs', 'knowledge-base', 'search'], + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 342fa9aeed279..3cd0d2acce4c9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -147,7 +147,14 @@ import type { HealthDiagnosticService } from './lib/telemetry/diagnostic/health_ import { ENTITY_RISK_SCORE_TOOL_ID } from './assistant/tools/entity_risk_score/entity_risk_score'; import type { TelemetryQueryConfiguration } from './lib/telemetry/types'; import { createAlertAttachmentType } from './agent_builder/attachments'; -import { alertsTool, evaluateAlertTool } from './agent_builder/tools'; +import { + alertsTool, + alertsIndexSearchTool, + evaluateAlertTool, + riskScoreSearchTool, + attackDiscoverySearchTool, + securityLabsSearchTool, +} from './agent_builder/tools'; export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract'; @@ -626,7 +633,11 @@ export class Plugin implements ISecuritySolutionPlugin { } if (plugins.onechat.tools) { plugins.onechat.tools.register(alertsTool()); + plugins.onechat.tools.register(alertsIndexSearchTool()); plugins.onechat.tools.register(evaluateAlertTool()); + plugins.onechat.tools.register(riskScoreSearchTool()); + plugins.onechat.tools.register(attackDiscoverySearchTool()); + plugins.onechat.tools.register(securityLabsSearchTool()); } } From 2b41a64f690b6df3a8d896c64675dbf428fe3927 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 19 Nov 2025 12:55:26 -0700 Subject: [PATCH 09/96] fixings --- .../security_solution/server/plugin.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 3cd0d2acce4c9..424fe43c7ac8b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -617,27 +617,32 @@ export class Plugin implements ISecuritySolutionPlugin { // Note: The alert attachment type may already be registered by onechat's built-in types. // If so, we'll skip registration and use the built-in version. if (plugins.onechat) { - if (plugins.onechat.attachments) { - try { - plugins.onechat.attachments.registerType(createAlertAttachmentType()); - } catch (error) { - // Alert attachment type may already be registered by onechat's built-in types - if (error instanceof Error && error.message.includes('already registered')) { - this.logger.debug( - 'Alert attachment type already registered by onechat plugin, using built-in version' - ); - } else { - throw error; - } + try { + // Register attachment type + plugins.onechat.attachments.registerType(createAlertAttachmentType()); + } catch (error) { + // Alert attachment type may already be registered by onechat's built-in types + if (error instanceof Error && error.message.includes('already registered')) { + this.logger.debug( + 'Alert attachment type already registered by onechat plugin, using built-in version' + ); + } else { + this.logger.warn(`Failed to register alert attachment type: ${error}`); + // Don't throw - allow plugin to continue loading even if attachment registration fails } } - if (plugins.onechat.tools) { + + // Register tools + try { plugins.onechat.tools.register(alertsTool()); plugins.onechat.tools.register(alertsIndexSearchTool()); plugins.onechat.tools.register(evaluateAlertTool()); plugins.onechat.tools.register(riskScoreSearchTool()); plugins.onechat.tools.register(attackDiscoverySearchTool()); plugins.onechat.tools.register(securityLabsSearchTool()); + } catch (error) { + this.logger.warn(`Failed to register onechat tools: ${error}`); + // Don't throw - allow plugin to continue loading even if tool registration fails } } From 372ffc9db8b5bf78be48176bab1b07eade946146 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 19 Nov 2025 13:26:27 -0700 Subject: [PATCH 10/96] add core alert attachment type --- .../agent_builder/attachments/core-alert.ts | 87 +++++++++++++++++++ .../server/agent_builder/attachments/index.ts | 3 +- 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts new file mode 100644 index 0000000000000..8f1b973a1b9a9 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts @@ -0,0 +1,87 @@ +/* + * 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. + */ + +import type { AlertAttachmentData } from '@kbn/onechat-common/attachments'; +import { AttachmentType, alertAttachmentDataSchema } from '@kbn/onechat-common/attachments'; +import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import { platformCoreTools } from '@kbn/onechat-common/tools'; + +/** + * Creates the definition for the `alert` attachment type. + */ +export const createAlertAttachmentType = (): AttachmentTypeDefinition< + AttachmentType.alert, + AlertAttachmentData +> => { + return { + id: AttachmentType.alert, + validate: (input) => { + const parseResult = alertAttachmentDataSchema.safeParse(input); + if (parseResult.success) { + return { valid: true, data: parseResult.data }; + } else { + return { valid: false, error: parseResult.error.message }; + } + }, + format: (attachment) => { + return { + getRepresentation: () => { + return { type: 'text', value: formatAlertData(attachment.data) }; + }, + }; + }, + getTools: () => { + const tools = [ + platformCoreTools.generateEsql, + platformCoreTools.executeEsql, + platformCoreTools.search, + ]; + return tools; + }, + getAgentDescription: () => { + const description = `You have access to security alert data. To provide a comprehensive analysis, you MUST gather enriched context by querying for related information. + +SECURITY ALERT DATA: +{alertData} + +--- + +MANDATORY WORKFLOW - Complete in order: + +1. Extract entities: host.name, user.name, source.ip, destination.ip, file.hash.sha256, kibana.alert.uuid (or _id), kibana.alert.rule.name, kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, event.category, event.action + +2. Query RELATED ALERTS: + Tool: ${platformCoreTools.search} + Parameters: { query: "Find security alerts from last 7 days where host.name is '[host]' OR user.name is '[user]' OR source.ip is '[ip]' OR destination.ip is '[dest_ip]'", index: ".alerts-security.alerts-*" } + +3. Query RISK SCORES: + Tool: ${platformCoreTools.search} + Parameters: { query: "Find risk scores for host.name '[host]' OR user.name '[user]'", index: "risk-score.risk-score-latest-default" } + +4. Query ATTACK DISCOVERIES: + Tool: ${platformCoreTools.search} + Parameters: { query: "Find attack discoveries where kibana.alert.attack_discovery.alert_ids contains '[alert ID]'", index: ".alerts-security.alerts-attack.discovery-*,.adhoc.alerts-security.alerts-attack.discovery-*" } + +5. Query SECURITY LABS: + Tool: ${platformCoreTools.search} + Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]", index: ".kibana-elastic-ai-assistant-knowledge-base-*" } + +CRITICAL: You MUST call all 4 tools (steps 2-5) before responding. Do not skip any step.`; + return description; + }, + }; +}; + +/** + * Formats alert data for display. + * + * @param data - The alert attachment data containing the alert string + * @returns Formatted string representation of the alert data + */ +const formatAlertData = (data: AlertAttachmentData): string => { + return data.alert; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts index e04586e444515..a98a20a84092b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts @@ -6,5 +6,4 @@ */ export { createAlertAttachmentType } from './alert'; - - +// export { createAlertAttachmentType } from './core-alert'; From 8a302debcb960e33b51713aee7fca1fa98743197 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 19 Nov 2025 13:30:33 -0700 Subject: [PATCH 11/96] Pierre change --- .../onechat/server/services/agents/modes/default/prompts.ts | 5 ++--- .../services/agents/modes/utils/prepare_conversation.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/prompts.ts b/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/prompts.ts index f7d4a07f69515..6dfda8703aa9a 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/prompts.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/prompts.ts @@ -254,10 +254,9 @@ const renderAttachmentTypeInstructions = (attachmentTypes: ProcessedAttachmentTy return ''; } - const perTypeInstructions = attachmentTypes.map(({ type, agentDescription }) => { + const perTypeInstructions = attachmentTypes.map(({ type, description }) => { return `### ${type} attachments - -${agentDescription ?? 'No instructions available.'} +${description ?? 'No instructions available.'} `; }); diff --git a/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/utils/prepare_conversation.ts b/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/utils/prepare_conversation.ts index 18a2a6558c910..22f0dd7d8d263 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/utils/prepare_conversation.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/utils/prepare_conversation.ts @@ -19,7 +19,7 @@ export interface ProcessedAttachment { export interface ProcessedAttachmentType { type: string; - agentDescription?: string; + description?: string; } export interface ProcessedRoundInput { From f10412253b84a49a9fdd68080cf7d2c4c45716e9 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 19 Nov 2025 14:23:38 -0700 Subject: [PATCH 12/96] core alert index hardcoded --- .../server/agent_builder/attachments/core-alert.ts | 6 +++--- .../server/agent_builder/attachments/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts index 8f1b973a1b9a9..7364f85316fb5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts @@ -56,7 +56,7 @@ MANDATORY WORKFLOW - Complete in order: 2. Query RELATED ALERTS: Tool: ${platformCoreTools.search} - Parameters: { query: "Find security alerts from last 7 days where host.name is '[host]' OR user.name is '[user]' OR source.ip is '[ip]' OR destination.ip is '[dest_ip]'", index: ".alerts-security.alerts-*" } + Parameters: { query: "Find security alerts from last 7 days where host.name is '[host]' OR user.name is '[user]' OR source.ip is '[ip]' OR destination.ip is '[dest_ip]'", index: ".alerts-security.alerts-default" } 3. Query RISK SCORES: Tool: ${platformCoreTools.search} @@ -64,11 +64,11 @@ MANDATORY WORKFLOW - Complete in order: 4. Query ATTACK DISCOVERIES: Tool: ${platformCoreTools.search} - Parameters: { query: "Find attack discoveries where kibana.alert.attack_discovery.alert_ids contains '[alert ID]'", index: ".alerts-security.alerts-attack.discovery-*,.adhoc.alerts-security.alerts-attack.discovery-*" } + Parameters: { query: "Find attack discoveries where kibana.alert.attack_discovery.alert_ids contains '[alert ID]'", index: ".alerts-security.alerts-attack.discovery-default,.adhoc.alerts-security.alerts-attack.discovery-default" } 5. Query SECURITY LABS: Tool: ${platformCoreTools.search} - Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]", index: ".kibana-elastic-ai-assistant-knowledge-base-*" } + Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]", index: ".kibana-elastic-ai-assistant-knowledge-base-default" } CRITICAL: You MUST call all 4 tools (steps 2-5) before responding. Do not skip any step.`; return description; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts index a98a20a84092b..0e953df6c4697 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { createAlertAttachmentType } from './alert'; -// export { createAlertAttachmentType } from './core-alert'; +// export { createAlertAttachmentType } from './alert'; +export { createAlertAttachmentType } from './core-alert'; From 469dc18d598a7f96f7270b665ce9c98adbeb8876 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 07:37:10 -0700 Subject: [PATCH 13/96] attack discovery tool improvements --- .../attack_discovery_search_tool.ts | 75 ++++++------------- 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery/attack_discovery_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery/attack_discovery_search_tool.ts index 2ebbb6801d7b5..33af00b876633 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery/attack_discovery_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery/attack_discovery_search_tool.ts @@ -8,44 +8,19 @@ import { z } from '@kbn/zod'; import { ToolType } from '@kbn/onechat-common'; import type { BuiltinToolDefinition } from '@kbn/onechat-server'; -import { executeEsql } from '@kbn/onechat-genai-utils/tools/utils/esql'; -import { ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX } from '@kbn/elastic-assistant-common'; +import { executeEsql } from '@kbn/onechat-genai-utils'; import { getSpaceIdFromRequest } from '../helpers'; import { securityTool } from '../constants'; const attackDiscoverySearchSchema = z.object({ - query: z - .string() + alertIds: z + .array(z.string()) .describe( - 'A natural language query expressing the search request for attack discoveries. Use this to find attack discoveries that include specific alert IDs in the kibana.alert.attack_discovery.alert_ids field. Include fields like kibana.alert.attack_discovery.title, kibana.alert.attack_discovery.summary_markdown, and kibana.alert.attack_discovery.alert_ids.' + 'An array of alert IDs to search for in attack discoveries. The tool will find attack discoveries where kibana.alert.attack_discovery.alert_ids contains any of the provided alert IDs.' ), }); -export const SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID = securityTool('attack-discovery-search'); - -/** - * Extracts alert IDs from a natural language query. - * Looks for patterns like "alert ID 'xxx'", "alert ID xxx", or quoted UUIDs. - */ -const extractAlertIds = (query: string): string[] => { - const alertIds: string[] = []; - - // Pattern 1: "alert ID 'xxx'" or "alert ID \"xxx\"" - const quotedPattern = /alert\s+id[:\s]+['"]([^'"]+)['"]/gi; - let match; - while ((match = quotedPattern.exec(query)) !== null) { - alertIds.push(match[1]); - } - - // Pattern 2: UUID pattern (8-4-4-4-12 hex digits) - const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi; - const uuidMatches = query.match(uuidPattern); - if (uuidMatches) { - alertIds.push(...uuidMatches); - } - - return [...new Set(alertIds)]; // Remove duplicates -}; +export const SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID = securityTool('attack_discovery_search'); export const attackDiscoverySearchTool = (): BuiltinToolDefinition< typeof attackDiscoverySearchSchema @@ -53,40 +28,33 @@ export const attackDiscoverySearchTool = (): BuiltinToolDefinition< return { id: SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, type: ToolType.builtin, - description: `Search and analyze attack discoveries. Use this tool to find attack discoveries related to specific alerts by searching for alert IDs in the kibana.alert.attack_discovery.alert_ids field. Automatically queries both scheduled and ad-hoc attack discovery indices for the current space. Limits results to 5 attack discoveries.`, + description: `Search and analyze attack discoveries. Use this tool to find attack discoveries related to specific alerts by providing alert IDs. The tool searches the kibana.alert.attack_discovery.alert_ids field. Automatically queries both scheduled and ad-hoc attack discovery indices for the current space. Limits results to 5 attack discoveries.`, schema: attackDiscoverySearchSchema, - handler: async ({ query: nlQuery }, { request, esClient, logger }) => { + handler: async ({ alertIds }, { request, esClient, logger }) => { const spaceId = getSpaceIdFromRequest(request); - logger.debug(`attack-discovery-search tool called with query: ${nlQuery}`); + logger.debug( + `attack-discovery-search tool called with alertIds: ${JSON.stringify(alertIds)}` + ); try { - // Extract alert IDs from the natural language query - const alertIds = extractAlertIds(nlQuery); - // Build date filter for last 7 days const now = new Date(); const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const dateFilter = `@timestamp >= "${sevenDaysAgo.toISOString()}" AND @timestamp <= "${now.toISOString()}"`; - // Build WHERE clause - let whereClause = dateFilter; - if (alertIds.length > 0) { - // Search for alert IDs in the array field - const alertIdConditions = alertIds - .map((id) => `"${id}" IN kibana.alert.attack_discovery.alert_ids`) - .join(' OR '); - whereClause = `${dateFilter} AND (${alertIdConditions})`; - } + // Build alert IDs filter using MV_CONTAINS with OR conditions + const alertIdsFilter = alertIds + .map((alertId) => `MV_CONTAINS(kibana.alert.attack_discovery.alert_ids,"${alertId}")`) + .join(' OR '); + + const whereClause = `${dateFilter} AND (${alertIdsFilter})`; - // Build ES|QL query - const esqlQuery = [ - `FROM ${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}*,.adhoc.${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}* METADATA _id`, - `| WHERE ${whereClause}`, - `| KEEP _id, kibana.alert.attack_discovery.title, kibana.alert.attack_discovery.summary_markdown, kibana.alert.workflow_status, kibana.alert.attack_discovery.alert_ids, kibana.alert.case_ids, @timestamp`, - `| SORT @timestamp DESC`, - `| LIMIT 5`, - ].join('\n'); + const esqlQuery = `FROM .alerts-security.attack.discovery.alerts-${spaceId}*,.adhoc.alerts-security.attack.discovery.alerts-${spaceId}* METADATA _id + | WHERE ${whereClause} + | KEEP _id, kibana.alert.attack_discovery.title, kibana.alert.severity, kibana.alert.workflow_status, kibana.alert.attack_discovery.alert_ids, kibana.alert.case_ids, @timestamp + | SORT @timestamp DESC + | LIMIT 100`; logger.debug(`Executing ES|QL query: ${esqlQuery}`); @@ -94,7 +62,6 @@ export const attackDiscoverySearchTool = (): BuiltinToolDefinition< query: esqlQuery, esClient: esClient.asCurrentUser, }); - console.log('ATT ==>', esqlResponse); const results = [ { From 832b7471e29e629832da049d5790baf80bceab44 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 08:10:50 -0700 Subject: [PATCH 14/96] fixing --- .../server/agent_builder/attachments/alert.ts | 39 +++-- .../server/agent_builder/attachments/index.ts | 4 +- .../tools/alerts/alerts_index_search_tool.ts | 23 +-- .../agent_builder/tools/alerts/alerts_tool.ts | 15 +- .../tools/alerts/alerts_tool2.ts | 136 --------------- .../tools/alerts/evaluate_alert_tool.ts | 18 +- .../attack_discovery_search_tool.ts | 4 +- .../server/agent_builder/tools/constants.ts | 125 ++++++++++++++ .../tools/entity_risk_score_tool.ts | 159 ++++++++++++++++++ .../server/agent_builder/tools/index.ts | 12 +- .../risk_score/risk_score_search_tool.ts | 67 -------- .../security_labs_search_tool.ts | 5 +- .../security_solution/server/plugin.ts | 4 +- 13 files changed, 325 insertions(+), 286 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool2.ts rename x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/{attack_discovery => }/attack_discovery_search_tool.ts (97%) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/risk_score/risk_score_search_tool.ts rename x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/{security_labs => }/security_labs_search_tool.ts (96%) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index da570d3508361..5c84d3a9e9313 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -7,10 +7,12 @@ import type { AlertAttachmentData } from '@kbn/onechat-common/attachments'; import { AttachmentType, alertAttachmentDataSchema } from '@kbn/onechat-common/attachments'; +import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import { platformCoreTools } from '@kbn/onechat-common'; import { SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID, - SECURITY_RISK_SCORE_SEARCH_TOOL_ID, + SECURITY_ENTITY_RISK_SCORE_TOOL_ID, SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, SECURITY_LABS_SEARCH_TOOL_ID, } from '../tools'; @@ -42,7 +44,7 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition< getTools: () => { const tools = [ SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID, - SECURITY_RISK_SCORE_SEARCH_TOOL_ID, + SECURITY_ENTITY_RISK_SCORE_TOOL_ID, SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, SECURITY_LABS_SEARCH_TOOL_ID, ]; @@ -55,28 +57,29 @@ SECURITY ALERT DATA: {alertData} --- - +user.risk.calculated_level MANDATORY WORKFLOW - Complete in order: -1. Extract entities: host.name, user.name, source.ip, destination.ip, file.hash.sha256, kibana.alert.uuid (or _id), kibana.alert.rule.name, kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, event.category, event.action - -2. Query RELATED ALERTS: - Tool: ${SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID} - Parameters: { query: "Find security alerts from last 7 days where host.name is '[host]' OR user.name is '[user]' OR source.ip is '[ip]' OR destination.ip is '[dest_ip]'" } +1. Extract alert id: kibana.alert.uuid or _id +2. Extract entities:host.name, host.ip, host.os.name, host.os.version, user.name, user.domain, source.ip, destination.ip, file.hash.sha256 +3. Extract MITRE fields: kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, threat.tactic.id +4. Query RISK SCORES for entities: + Tool: ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} + Parameters: { identifierType: "host.name", identifier: "MyHostName" } -3. Query RISK SCORES: - Tool: ${SECURITY_RISK_SCORE_SEARCH_TOOL_ID} - Parameters: { query: "Find risk scores for host.name '[host]' OR user.name '[user]'. Include host.risk.calculated_score_norm, host.risk.calculated_level, user.risk.calculated_score_norm, and user.risk.calculated_level fields." } +5. Query ATTACK DISCOVERIES for the extracted alert id: + Tool: ${sanitizeToolId(SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID)} + Parameters: { alertIds: ["[alert ID]"] } -4. Query ATTACK DISCOVERIES: - Tool: ${SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID} - Parameters: { query: "Find attack discoveries where kibana.alert.attack_discovery.alert_ids contains '[alert ID]'. Include kibana.alert.attack_discovery.title, kibana.alert.attack_discovery.summary_markdown, and kibana.alert.attack_discovery.alert_ids fields." } - -5. Query SECURITY LABS: - Tool: ${SECURITY_LABS_SEARCH_TOOL_ID} +6. Query SECURITY LABS: + Tool: ${sanitizeToolId(SECURITY_LABS_SEARCH_TOOL_ID)} Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]" } -CRITICAL: You MUST call all 4 tools (steps 2-5) before responding. Do not skip any step.`; +7. Query PRODUCT DOCUMENTATION: + Tool: ${sanitizeToolId(platformCoreTools.productDocumentation)} + Parameters: { query: "Find alert triage steps", product: "security" } + +CRITICAL: You MUST call all 4 tools (steps 4-7) before responding. Do not skip any step.`; return description; }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts index 0e953df6c4697..a98a20a84092b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -// export { createAlertAttachmentType } from './alert'; -export { createAlertAttachmentType } from './core-alert'; +export { createAlertAttachmentType } from './alert'; +// export { createAlertAttachmentType } from './core-alert'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts index 220991782b897..333ac84be6b96 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts @@ -11,7 +11,7 @@ import { generateEsql } from '@kbn/onechat-genai-utils/tools'; import type { BuiltinToolDefinition } from '@kbn/onechat-server'; import { DEFAULT_ALERTS_INDEX } from '../../../../common/constants'; import { getSpaceIdFromRequest } from '../helpers'; -import { securityTool } from '../constants'; +import { ESSENTIAL_ALERT_FIELDS, securityTool } from '../constants'; const alertsIndexSearchSchema = z.object({ query: z @@ -21,20 +21,9 @@ const alertsIndexSearchSchema = z.object({ ), }); -export const SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID = securityTool('alerts-index-search'); +export const SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID = securityTool('alerts_index_search'); -const KEEP_FIELDS = [ - '@timestamp', - 'host.name', - 'user.name', - 'kibana.alert.rule.name', - 'kibana.alert.severity', - 'kibana.alert.risk_score', - 'source.ip', - 'destination.ip', - 'event.category', - 'message', -].join(', '); +const KEEP_FIELDS = ESSENTIAL_ALERT_FIELDS.join(', '); const ADDITIONAL_INSTRUCTIONS = `When querying security alert indices, ALWAYS use the KEEP command to filter fields and reduce response size. Include these essential fields: ${KEEP_FIELDS}. Limit results to 50 alerts. Example: FROM .alerts-security.alerts-* | KEEP ${KEEP_FIELDS} | ...`; @@ -44,10 +33,7 @@ export const alertsIndexSearchTool = (): BuiltinToolDefinition { + handler: async ({ query: nlQuery }, { request, esClient, modelProvider, logger, events }) => { const spaceId = getSpaceIdFromRequest(request); const searchIndex = `${DEFAULT_ALERTS_INDEX}-${spaceId}`; @@ -129,4 +115,3 @@ export const alertsIndexSearchTool = (): BuiltinToolDefinition => { - return { - id: SECURITY_ALERTS_TOOL_ID, - type: ToolType.builtin, - description: `Search and analyze security alerts using full-text or structured queries for finding, counting, aggregating, or summarizing alerts.`, - schema: alertsSchema, - handler: async ( - { query: nlQuery, index }, - { request, esClient, modelProvider, logger, events } - ) => { - // Determine the index to use: either explicitly provided or based on the current space - const spaceId = getSpaceIdFromRequest(request); - const searchIndex = index ?? `${DEFAULT_ALERTS_INDEX}-${spaceId}`; - - logger.debug(`alerts tool called with query: ${nlQuery}, index: ${searchIndex}`); - - try { - // Generate ES|QL query with automatic KEEP field filtering - const esqlResponse = await generateEsql({ - nlQuery, - index: searchIndex, - additionalInstructions: ADDITIONAL_INSTRUCTIONS, - executeQuery: true, - model: await modelProvider.getDefaultModel(), - esClient: esClient.asCurrentUser, - logger, - events, - }); - - if (esqlResponse.error) { - return { - results: [ - { - type: 'error', - data: { - message: esqlResponse.error, - }, - }, - ], - }; - } - - const results = []; - - if (esqlResponse.query) { - results.push({ - type: 'query', - data: { - esql: esqlResponse.query, - }, - }); - } - - if (esqlResponse.results) { - results.push({ - type: 'tabularData', - data: { - source: 'esql', - query: esqlResponse.query || '', - columns: esqlResponse.results.columns, - values: esqlResponse.results.values, - }, - }); - } - - if (esqlResponse.answer) { - results.push({ - type: 'other', - data: { - answer: esqlResponse.answer, - }, - }); - } - - return { results }; - } catch (error) { - logger.error(`Error in alerts tool: ${error.message}`); - return { - results: [ - { - type: 'error', - data: { - message: `Error: ${error.message}`, - }, - }, - ], - }; - } - }, - tags: ['security', 'alerts'], - }; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts index 2a23a50848dc7..ac5469a7c02b4 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts @@ -17,7 +17,7 @@ import { ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX } from '@kbn/elastic-assist import { DEFAULT_ALERTS_INDEX } from '../../../../common/constants'; import { getRiskIndex } from '../../../../common/search_strategy/security_solution/risk_score/common'; import { getSpaceIdFromRequest } from '../helpers'; -import { securityTool } from '../constants'; +import { ESSENTIAL_ALERT_FIELDS, securityTool } from '../constants'; const evaluateAlertSchema = z.object({ alertData: z @@ -27,22 +27,10 @@ const evaluateAlertSchema = z.object({ ), }); -export const EVALUATE_ALERT_TOOL_ID = securityTool('evaluate-alert'); +export const EVALUATE_ALERT_TOOL_ID = securityTool('evaluate_alert'); // Essential fields to keep when querying alerts to minimize token usage -const KEEP_FIELDS = [ - '_id', - '@timestamp', - 'host.name', - 'user.name', - 'kibana.alert.rule.name', - 'kibana.alert.severity', - 'kibana.alert.risk_score', - 'source.ip', - 'destination.ip', - 'event.category', - 'message', -].join(', '); +const KEEP_FIELDS = ESSENTIAL_ALERT_FIELDS.join(', '); const ENTITY_EXTRACTION_PROMPT = `Extract security entities from the following alert data. Return a JSON object with the following structure: { diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery/attack_discovery_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts similarity index 97% rename from x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery/attack_discovery_search_tool.ts rename to x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts index 33af00b876633..1c25c94536771 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery/attack_discovery_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts @@ -9,8 +9,8 @@ import { z } from '@kbn/zod'; import { ToolType } from '@kbn/onechat-common'; import type { BuiltinToolDefinition } from '@kbn/onechat-server'; import { executeEsql } from '@kbn/onechat-genai-utils'; -import { getSpaceIdFromRequest } from '../helpers'; -import { securityTool } from '../constants'; +import { getSpaceIdFromRequest } from './helpers'; +import { securityTool } from './constants'; const attackDiscoverySearchSchema = z.object({ alertIds: z diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts index b4645048ad03b..a5bb11201b342 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts @@ -13,3 +13,128 @@ import { internalNamespaces } from '@kbn/onechat-common/base/namespaces'; export const securityTool = (toolName: string): string => { return `${internalNamespaces.coreSecurity}.${toolName}`; }; + +/** + * Essential fields to return for security alerts to reduce context window usage. + * These fields contain the most relevant information for security analysis. + */ +export const ESSENTIAL_ALERT_FIELDS = [ + '@timestamp', + 'message', + + /* Host */ + 'host.name', + 'host.ip', + 'host.os.name', + 'host.os.version', + 'host.asset.criticality', + 'host.risk.calculated_level', + 'host.risk.calculated_score_norm', + + /* User */ + 'user.name', + 'user.domain', + 'user.asset.criticality', + 'user.risk.calculated_level', + 'user.risk.calculated_score_norm', + 'user.target.name', + + /* Process */ + 'process.name', + 'process.pid', + 'process.args', + 'process.command_line', + 'process.executable', + 'process.exit_code', + 'process.working_directory', + 'process.pe.original_file_name', + 'process.hash.md5', + 'process.hash.sha1', + 'process.hash.sha256', + 'process.code_signature.exists', + 'process.code_signature.signing_id', + 'process.code_signature.status', + 'process.code_signature.subject_name', + 'process.code_signature.trusted', + + /* Process parent */ + 'process.parent.name', + 'process.parent.args', + 'process.parent.args_count', + 'process.parent.command_line', + 'process.parent.executable', + 'process.parent.code_signature.exists', + 'process.parent.code_signature.status', + 'process.parent.code_signature.subject_name', + 'process.parent.code_signature.trusted', + + /* File */ + 'file.name', + 'file.path', + 'file.hash.sha256', + + /* Groups */ + 'group.id', + 'group.name', + + /* Cloud */ + 'cloud.provider', + 'cloud.region', + 'cloud.availability_zone', + + /* Network / DNS */ + 'source.ip', + 'destination.ip', + 'network.protocol', + 'dns.question.name', + 'dns.question.type', + + /* Event */ + 'event.category', + 'event.action', + 'event.type', + 'event.code', + 'event.dataset', + 'event.module', + 'event.outcome', + + /* Rule (generic) */ + 'rule.name', + 'rule.reference', + + /* Kibana alert fields */ + 'kibana.alert.uuid', + 'kibana.alert.original_time', + 'kibana.alert.severity', + 'kibana.alert.start', + 'kibana.alert.workflow_status', + 'kibana.alert.reason', + 'kibana.alert.risk_score', + 'kibana.alert.rule.name', + 'kibana.alert.rule.rule_id', + 'kibana.alert.rule.description', + 'kibana.alert.rule.category', + 'kibana.alert.rule.references', + 'kibana.alert.rule.threat.framework', + 'kibana.alert.rule.threat.tactic.id', + 'kibana.alert.rule.threat.tactic.name', + 'kibana.alert.rule.threat.tactic.reference', + 'kibana.alert.rule.threat.technique.id', + 'kibana.alert.rule.threat.technique.name', + 'kibana.alert.rule.threat.technique.reference', + 'kibana.alert.rule.threat.technique.subtechnique.id', + 'kibana.alert.rule.threat.technique.subtechnique.name', + 'kibana.alert.rule.threat.technique.subtechnique.reference', + + /* Threat (top-level) */ + 'threat.framework', + 'threat.tactic.id', + 'threat.tactic.name', + 'threat.tactic.reference', + 'threat.technique.id', + 'threat.technique.name', + 'threat.technique.reference', + 'threat.technique.subtechnique.id', + 'threat.technique.subtechnique.name', + 'threat.technique.subtechnique.reference', +] as const; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts new file mode 100644 index 0000000000000..62a509c5d524d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts @@ -0,0 +1,159 @@ +/* + * 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. + */ + +import type { ElasticsearchClient } from '@kbn/core/server'; +import { z } from '@kbn/zod'; +import { ToolType, ToolResultType } from '@kbn/onechat-common'; +import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import { getToolResultId } from '@kbn/onechat-server/tools'; +import { IdentifierType } from '../../../common/api/entity_analytics/common/common.gen'; +import { createGetRiskScores } from '../../lib/entity_analytics/risk_score/get_risk_score'; +import type { EntityType } from '../../../common/entity_analytics/types'; +import { DEFAULT_ALERTS_INDEX } from '../../../common/constants'; +import { getSpaceIdFromRequest } from './helpers'; +import { ESSENTIAL_ALERT_FIELDS, securityTool } from './constants'; + +const entityRiskScoreSchema = z.object({ + identifierType: IdentifierType.describe('The type of entity: host, user, service, or generic'), + identifier: z + .string() + .min(1) + .describe('The value that identifies the entity (e.g., hostname, username)'), +}); + +export const SECURITY_ENTITY_RISK_SCORE_TOOL_ID = securityTool('entity_risk_score'); + +/** + * Fetches alerts by their IDs, returning only essential fields for risk score context + */ +const getAlertsById = async ({ + esClient, + index, + ids, +}: { + esClient: ElasticsearchClient; + index: string; + ids: string[]; +}): Promise> => { + if (ids.length === 0) { + return {}; + } + + const response = await esClient.search({ + index, + ignore_unavailable: true, + allow_no_indices: true, + size: ids.length, + _source: ESSENTIAL_ALERT_FIELDS, + query: { + bool: { + filter: [{ terms: { _id: ids } }], + }, + }, + }); + + return response.hits.hits.reduce>((acc, hit) => { + if (hit._source && hit._id) { + acc[hit._id] = hit._source; + } + return acc; + }, {}); +}; + +export const entityRiskScoreTool = (): BuiltinToolDefinition => { + return { + id: SECURITY_ENTITY_RISK_SCORE_TOOL_ID, + type: ToolType.builtin, + description: `Call this tool to get the latest entity risk score and the inputs that contributed to the calculation for a specific entity (host, user, service, or generic). The risk score is sorted by 'kibana.alert.risk_score'. When reporting the risk score value, use the normalized field 'calculated_score_norm' which ranges from 0-100.`, + schema: entityRiskScoreSchema, + handler: async ({ identifierType, identifier }, { request, esClient, logger }) => { + const spaceId = getSpaceIdFromRequest(request); + const alertsIndexPattern = `${DEFAULT_ALERTS_INDEX}-${spaceId}`; + + logger.debug( + `entity-risk-score tool called with identifierType: ${identifierType}, identifier: ${identifier}` + ); + + try { + const getRiskScore = createGetRiskScores({ + logger, + esClient: esClient.asCurrentUser, + spaceId, + }); + + const riskScores = await getRiskScore({ + entityType: identifierType as EntityType, + entityIdentifier: identifier, + pagination: { querySize: 1, cursorStart: 0 }, + }); + + if (riskScores.length === 0) { + return { + results: [ + { + tool_result_id: getToolResultId(), + type: ToolResultType.error, + data: { + message: `No risk score found for ${identifierType} entity with identifier: ${identifier}`, + }, + }, + ], + }; + } + + const latestRiskScore = riskScores[0]; + + // Fetch all alerts that contributed to the risk score to enhance the inputs + const alertIds = latestRiskScore.inputs.map((i) => i.id).filter(Boolean); + const alertsById = await getAlertsById({ + esClient: esClient.asCurrentUser, + index: alertsIndexPattern, + ids: alertIds, + }); + + // Enhance inputs with alert data + const enhancedInputs = latestRiskScore.inputs.map((input) => ({ + risk_score: input.risk_score, + contribution_score: input.contribution_score, + category: input.category, + alert_contribution: alertsById[input.id] || null, + })); + + const riskScoreData = { + ...latestRiskScore, + inputs: enhancedInputs, + }; + + return { + results: [ + { + tool_result_id: getToolResultId(), + type: ToolResultType.other, + data: { + riskScore: riskScoreData, + }, + }, + ], + }; + } catch (error) { + logger.error(`Error in entity-risk-score tool: ${error.message}`); + return { + results: [ + { + tool_result_id: getToolResultId(), + type: ToolResultType.error, + data: { + message: `Error fetching risk score: ${error.message}`, + }, + }, + ], + }; + } + }, + tags: ['security', 'entity-risk-score', 'entities'], + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts index 074c33cb4dded..830e99f40ed10 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts @@ -11,15 +11,9 @@ export { SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID, } from './alerts/alerts_index_search_tool'; export { evaluateAlertTool, EVALUATE_ALERT_TOOL_ID } from './alerts/evaluate_alert_tool'; -export { - riskScoreSearchTool, - SECURITY_RISK_SCORE_SEARCH_TOOL_ID, -} from './risk_score/risk_score_search_tool'; +export { entityRiskScoreTool, SECURITY_ENTITY_RISK_SCORE_TOOL_ID } from './entity_risk_score_tool'; export { attackDiscoverySearchTool, SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, -} from './attack_discovery/attack_discovery_search_tool'; -export { - securityLabsSearchTool, - SECURITY_LABS_SEARCH_TOOL_ID, -} from './security_labs/security_labs_search_tool'; +} from './attack_discovery_search_tool'; +export { securityLabsSearchTool, SECURITY_LABS_SEARCH_TOOL_ID } from './security_labs_search_tool'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/risk_score/risk_score_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/risk_score/risk_score_search_tool.ts deleted file mode 100644 index dbdea3cc9099c..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/risk_score/risk_score_search_tool.ts +++ /dev/null @@ -1,67 +0,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 Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod'; -import { ToolType } from '@kbn/onechat-common'; -import type { BuiltinToolDefinition } from '@kbn/onechat-server'; -import { runSearchTool } from '@kbn/onechat-genai-utils/tools/search/run_search_tool'; -import { getRiskIndex } from '../../../../common/search_strategy/security_solution/risk_score/common'; -import { getSpaceIdFromRequest } from '../helpers'; -import { securityTool } from '../constants'; - -const riskScoreSearchSchema = z.object({ - query: z - .string() - .describe( - 'A natural language query expressing the search request for risk scores. Use this to find risk scores for hosts (host.name) or users (user.name). Include fields like host.risk.calculated_score_norm, host.risk.calculated_level, user.risk.calculated_score_norm, user.risk.calculated_level.' - ), -}); - -export const SECURITY_RISK_SCORE_SEARCH_TOOL_ID = securityTool('risk-score-search'); - -export const riskScoreSearchTool = (): BuiltinToolDefinition => { - return { - id: SECURITY_RISK_SCORE_SEARCH_TOOL_ID, - type: ToolType.builtin, - description: `Search and analyze risk scores for hosts and users. Use this tool to find risk score information including calculated_score_norm and calculated_level for entities. Automatically queries the latest risk score index for the current space.`, - schema: riskScoreSearchSchema, - handler: async ({ query: nlQuery }, { request, esClient, modelProvider, logger, events }) => { - const spaceId = getSpaceIdFromRequest(request); - const riskIndex = getRiskIndex(spaceId, true); - - logger.debug(`risk-score-search tool called with query: ${nlQuery}, index: ${riskIndex}`); - - try { - const results = await runSearchTool({ - nlQuery, - index: riskIndex, - model: await modelProvider.getDefaultModel(), - esClient: esClient.asCurrentUser, - logger, - events, - }); - - console.log('RISK ==>', results); - - return { results }; - } catch (error) { - logger.error(`Error in risk-score-search tool: ${error.message}`); - return { - results: [ - { - type: 'error', - data: { - message: `Error: ${error.message}`, - }, - }, - ], - }; - } - }, - tags: ['security', 'risk-score', 'search'], - }; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs/security_labs_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts similarity index 96% rename from x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs/security_labs_search_tool.ts rename to x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts index fbfac3b64b315..602beb8692042 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs/security_labs_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts @@ -10,7 +10,7 @@ import { ToolType } from '@kbn/onechat-common'; import type { BuiltinToolDefinition } from '@kbn/onechat-server'; import { runSearchTool } from '@kbn/onechat-genai-utils/tools/search/run_search_tool'; import { SECURITY_LABS_RESOURCE } from '@kbn/elastic-assistant-plugin/server/routes/knowledge_base/constants'; -import { securityTool } from '../constants'; +import { securityTool } from './constants'; const securityLabsSearchSchema = z.object({ query: z @@ -20,7 +20,7 @@ const securityLabsSearchSchema = z.object({ ), }); -export const SECURITY_LABS_SEARCH_TOOL_ID = securityTool('security-labs-search'); +export const SECURITY_LABS_SEARCH_TOOL_ID = securityTool('security_labs_search'); const SECURITY_LABS_INDEX_PATTERN = '.kibana-elastic-ai-assistant-knowledge-base-default'; @@ -47,7 +47,6 @@ export const securityLabsSearchTool = (): BuiltinToolDefinition< logger, events, }); - console.log('SL ==>', results); return { results }; } catch (error) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 0bb84ce829182..ad459d81806ca 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -151,7 +151,7 @@ import { alertsTool, alertsIndexSearchTool, evaluateAlertTool, - riskScoreSearchTool, + entityRiskScoreTool, attackDiscoverySearchTool, securityLabsSearchTool, } from './agent_builder/tools'; @@ -637,7 +637,7 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.onechat.tools.register(alertsTool()); plugins.onechat.tools.register(alertsIndexSearchTool()); plugins.onechat.tools.register(evaluateAlertTool()); - plugins.onechat.tools.register(riskScoreSearchTool()); + plugins.onechat.tools.register(entityRiskScoreTool()); plugins.onechat.tools.register(attackDiscoverySearchTool()); plugins.onechat.tools.register(securityLabsSearchTool()); } catch (error) { From fe9b3cfaabc1126bd79809febbc98d50277c0e66 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 09:18:14 -0700 Subject: [PATCH 15/96] fixing --- .../onechat/onechat-server/allow_lists.ts | 12 +++--- .../server/agent_builder/attachments/alert.ts | 40 +++++++++---------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts b/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts index 3b03d977bd559..97d22c1aad5f9 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts @@ -23,12 +23,12 @@ export const AGENT_BUILDER_BUILTIN_TOOLS: string[] = [ 'observability.get_services', 'observability.get_downstream_dependencies', // Security Solution - 'core.security.alerts', - 'core.security.alerts-index-search', - 'core.security.evaluate-alert', - 'core.security.risk-score-search', - 'core.security.attack-discovery-search', - 'core.security.security-labs-search', + // 'core.security.alerts', + // 'core.security.alerts_index_search', + // 'core.security.evaluate_alert', + 'core.security.entity_risk_score', + 'core.security.attack_discovery_search', + 'core.security.security_labs_search', ]; /** diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 5c84d3a9e9313..89fedb56ad0d6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -9,9 +9,7 @@ import type { AlertAttachmentData } from '@kbn/onechat-common/attachments'; import { AttachmentType, alertAttachmentDataSchema } from '@kbn/onechat-common/attachments'; import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; -import { platformCoreTools } from '@kbn/onechat-common'; import { - SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID, SECURITY_ENTITY_RISK_SCORE_TOOL_ID, SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, SECURITY_LABS_SEARCH_TOOL_ID, @@ -41,15 +39,11 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition< }, }; }, - getTools: () => { - const tools = [ - SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID, - SECURITY_ENTITY_RISK_SCORE_TOOL_ID, - SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, - SECURITY_LABS_SEARCH_TOOL_ID, - ]; - return tools; - }, + getTools: () => [ + SECURITY_ENTITY_RISK_SCORE_TOOL_ID, + SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, + SECURITY_LABS_SEARCH_TOOL_ID, + ], getAgentDescription: () => { const description = `You have access to security alert data. To provide a comprehensive analysis, you MUST gather enriched context by querying for related information. @@ -57,29 +51,31 @@ SECURITY ALERT DATA: {alertData} --- -user.risk.calculated_level MANDATORY WORKFLOW - Complete in order: 1. Extract alert id: kibana.alert.uuid or _id -2. Extract entities:host.name, host.ip, host.os.name, host.os.version, user.name, user.domain, source.ip, destination.ip, file.hash.sha256 -3. Extract MITRE fields: kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, threat.tactic.id -4. Query RISK SCORES for entities: +2. Extract rule name: kibana.alert.rule.name +3. Extract entities: host.name, host.ip, host.os.name, host.os.version, user.name, user.domain, source.ip, destination.ip, file.hash.sha256 +4. Extract MITRE fields: kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, threat.tactic.id +5. Query RISK SCORES for entities: Tool: ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} Parameters: { identifierType: "host.name", identifier: "MyHostName" } -5. Query ATTACK DISCOVERIES for the extracted alert id: +6. Query ATTACK DISCOVERIES for the extracted alert id: Tool: ${sanitizeToolId(SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID)} Parameters: { alertIds: ["[alert ID]"] } -6. Query SECURITY LABS: +7. Query SECURITY LABS: Tool: ${sanitizeToolId(SECURITY_LABS_SEARCH_TOOL_ID)} Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]" } -7. Query PRODUCT DOCUMENTATION: - Tool: ${sanitizeToolId(platformCoreTools.productDocumentation)} - Parameters: { query: "Find alert triage steps", product: "security" } - -CRITICAL: You MUST call all 4 tools (steps 4-7) before responding. Do not skip any step.`; +CRITICAL: You MUST call all 3 tools (steps 5-7) before responding. Do not skip any step.`; + // TODO add this once product doc tool available + // 8. Query PRODUCT DOCUMENTATION: + // Tool: ${sanitizeToolId(platformCoreTools.productDocumentation)} + // Parameters: { query: "Find alert triage steps", product: "security" } + // + // CRITICAL: You MUST call all 4 tools (steps 5-8) before responding. Do not skip any step.`; return description; }, }; From 37256be7bccbccd8cb1b8e3afe94509159ca1040 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 09:24:34 -0700 Subject: [PATCH 16/96] alert attachment works --- .../security/plugins/security_solution/server/plugin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index ad459d81806ca..9d135b5b848b8 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -634,9 +634,9 @@ export class Plugin implements ISecuritySolutionPlugin { // Register tools try { - plugins.onechat.tools.register(alertsTool()); - plugins.onechat.tools.register(alertsIndexSearchTool()); - plugins.onechat.tools.register(evaluateAlertTool()); + // plugins.onechat.tools.register(alertsTool()); + // plugins.onechat.tools.register(alertsIndexSearchTool()); + // plugins.onechat.tools.register(evaluateAlertTool()); plugins.onechat.tools.register(entityRiskScoreTool()); plugins.onechat.tools.register(attackDiscoverySearchTool()); plugins.onechat.tools.register(securityLabsSearchTool()); From a43976d7d9c856ee6186b52f34b888a89bd33834 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 09:36:07 -0700 Subject: [PATCH 17/96] entities --- .../server/agent_builder/attachments/alert.ts | 2 +- .../server/agent_builder/tools/constants.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 89fedb56ad0d6..489b42842a46b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -55,7 +55,7 @@ MANDATORY WORKFLOW - Complete in order: 1. Extract alert id: kibana.alert.uuid or _id 2. Extract rule name: kibana.alert.rule.name -3. Extract entities: host.name, host.ip, host.os.name, host.os.version, user.name, user.domain, source.ip, destination.ip, file.hash.sha256 +3. Extract entities: host.name, user.name, service.name, entity.id 4. Extract MITRE fields: kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, threat.tactic.id 5. Query RISK SCORES for entities: Tool: ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts index a5bb11201b342..7f9b3d13bc8ce 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts @@ -19,6 +19,7 @@ export const securityTool = (toolName: string): string => { * These fields contain the most relevant information for security analysis. */ export const ESSENTIAL_ALERT_FIELDS = [ + '_id', '@timestamp', 'message', @@ -39,6 +40,19 @@ export const ESSENTIAL_ALERT_FIELDS = [ 'user.risk.calculated_score_norm', 'user.target.name', + /* Service */ + 'service.name', + 'service.id', + + /* Entity */ + 'entity.id', + 'entity.name', + 'entity.type', + 'entity.sub_type', + + /* Agent */ + 'agent.id', + /* Process */ 'process.name', 'process.pid', @@ -71,6 +85,7 @@ export const ESSENTIAL_ALERT_FIELDS = [ /* File */ 'file.name', 'file.path', + 'file.Ext.original.path', 'file.hash.sha256', /* Groups */ @@ -79,6 +94,8 @@ export const ESSENTIAL_ALERT_FIELDS = [ /* Cloud */ 'cloud.provider', + 'cloud.account.name', + 'cloud.service.name', 'cloud.region', 'cloud.availability_zone', From 8294772d71e4d431e09bf29d820d08a267bac4ee Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 09:56:58 -0700 Subject: [PATCH 18/96] cases tool added to platform --- .../onechat/onechat-common/tools/constants.ts | 1 + .../plugins/shared/onechat/kibana.jsonc | 1 + .../tools/builtin/definitions/cases/cases.ts | 317 ++++++++++++++++++ .../builtin/definitions/cases/parse_query.ts | 84 +++++ .../tools/builtin/definitions/index.ts | 1 + .../services/tools/builtin/register_tools.ts | 2 + .../plugins/shared/onechat/server/types.ts | 2 + 7 files changed, 408 insertions(+) create mode 100644 x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts create mode 100644 x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/tools/constants.ts b/x-pack/platform/packages/shared/onechat/onechat-common/tools/constants.ts index 6c9893e012569..01f5e01ec5ece 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/tools/constants.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/tools/constants.ts @@ -24,6 +24,7 @@ export const platformCoreTools = { generateEsql: platformCoreTool('generate_esql'), executeEsql: platformCoreTool('execute_esql'), createVisualization: platformCoreTool('create_visualization'), + cases: platformCoreTool('cases'), } as const; /** diff --git a/x-pack/platform/plugins/shared/onechat/kibana.jsonc b/x-pack/platform/plugins/shared/onechat/kibana.jsonc index 4d533558a21f9..50c4d42031362 100644 --- a/x-pack/platform/plugins/shared/onechat/kibana.jsonc +++ b/x-pack/platform/plugins/shared/onechat/kibana.jsonc @@ -34,6 +34,7 @@ "kibanaReact" ], "optionalPlugins": [ + "cases", "cloud", "licenseManagement", "workflowsManagement", diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts new file mode 100644 index 0000000000000..5fc3e8a0d23e8 --- /dev/null +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts @@ -0,0 +1,317 @@ +/* + * 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. + */ + +import { z } from '@kbn/zod'; +import { platformCoreTools, ToolType } from '@kbn/onechat-common'; +import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import { createErrorResult } from '@kbn/onechat-server'; +import { ToolResultType } from '@kbn/onechat-common/tools/tool_result'; +import type { CoreSetup } from '@kbn/core/server'; +import type { OnechatStartDependencies, OnechatPluginStart } from '../../../types'; +import { parseCasesQuery } from './parse_query'; + +const casesSchema = z.object({ + owner: z + .enum(['cases', 'observability', 'securitySolution']) + .optional() + .describe( + 'Filter cases by owner. Valid values: "cases" (Stack Management/General Cases), "observability" (Observability), "securitySolution" (Elastic Security). If not provided, returns all cases the user has access to.' + ), + query: z + .string() + .describe('Natural language query describing which cases to retrieve (e.g., "cases updated in the last week", "cases from November 2nd")'), +}); + +const getUsername = (user: any): string | null => { + return user?.username || null; +}; + +const getCaseTimestamp = (caseItem: any): number | null => { + const timestampValue = caseItem.updated_at ?? caseItem.created_at; + if (!timestampValue) return null; + const timestamp = new Date(timestampValue).getTime(); + return isNaN(timestamp) ? null : timestamp; +}; + +const normalizeTimeRange = ( + start: string | undefined, + end: string | undefined, + logger: any +): { start: string; end: string | null; startDate: Date; endDate: Date | null } => { + const now = new Date(); + const currentYear = now.getFullYear(); + + let startDate: Date; + if (start) { + // If no year is specified, assume current year + const startStr = start.includes('T') && !start.match(/^\d{4}/) ? `${currentYear}-${start}` : start; + startDate = new Date(startStr); + if (isNaN(startDate.getTime())) { + logger.warn(`Invalid start date: ${start}, using default`); + startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // Default to 7 days ago + } + } else { + startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // Default to 7 days ago + } + + let endDate: Date | null = null; + if (end) { + const endStr = end.includes('T') && !end.match(/^\d{4}/) ? `${currentYear}-${end}` : end; + endDate = new Date(endStr); + if (isNaN(endDate.getTime())) { + logger.warn(`Invalid end date: ${end}, using now`); + endDate = null; + } + } + + return { + start: startDate.toISOString(), + end: endDate ? endDate.toISOString() : null, + startDate, + endDate, + }; +}; + +const createEmptyResults = ( + normalizedStart: string, + normalizedEnd: string | null, + message: string +) => ({ + results: [ + { + type: ToolResultType.other, + data: { + cases: [], + total: 0, + start: normalizedStart, + end: normalizedEnd || null, + message, + }, + }, + ], +}); + +export const casesTool = ( + coreSetup: CoreSetup +): BuiltinToolDefinition => { + return { + id: platformCoreTools.cases, + type: ToolType.builtin, + description: `Retrieves cases from Elastic Security, Observability, or Stack Management based on a natural language query. + +The 'query' parameter should be a natural language description of which cases to retrieve. Examples: +- "cases updated in the last week" +- "cases from November 2nd" +- "cases updated between January 1st and January 15th" +- "recent cases" + +The optional 'owner' parameter filters cases by owner: "cases" (Stack Management/General Cases), "observability" (Observability), or "securitySolution" (Elastic Security). If not provided, returns all cases the user has access to. + +Returns cases with detailed information including id, title, description, status, severity, tags, assignees, observables, total alerts/comments, and recent comments. Each case includes a URL for direct access. + +**IMPORTANT**: When presenting case results to the user, provide a short paragraph summary (2-3 sentences) describing the key details of each case, then include a clickable link to the case using the provided URL.`, + schema: casesSchema, + handler: async ({ owner, query }, { request, logger, modelProvider }) => { + try { + // Parse natural language query to extract date ranges + const model = await modelProvider.getDefaultModel(); + const parsedQuery = await parseCasesQuery({ + nlQuery: query, + model, + logger, + }); + + // Normalize and adjust time range + const timeRange = normalizeTimeRange(parsedQuery.start, parsedQuery.end, logger); + + // Get cases plugin from start services + const [, plugins] = await coreSetup.getStartServices(); + const casesPlugin = plugins.cases; + + if (!casesPlugin) { + logger.warn('[Cases Tool] Cases plugin not available, returning empty results'); + return createEmptyResults( + timeRange.start, + timeRange.end, + 'Cases plugin not available' + ); + } + + // Get cases client + const casesClient = await casesPlugin.getCasesClientWithRequest(request); + + // Use Cases API search + const searchParams: any = { + sortField: 'updatedAt', + sortOrder: 'desc', + perPage: 100, + page: 1, + ...(owner && { owner }), + }; + + // Fetch cases with pagination + const allCases: any[] = []; + let currentPage = 1; + const maxPages = 10; + let hasMorePages = true; + const startTimestamp = timeRange.startDate.getTime(); + const endTimestamp = timeRange.endDate ? timeRange.endDate.getTime() : null; + + while (hasMorePages && currentPage <= maxPages) { + searchParams.page = currentPage; + const searchResult = await casesClient.cases.search(searchParams); + + if (searchResult.cases.length === 0) { + hasMorePages = false; + break; + } + + // Filter cases by updatedAt date range + const pageFilteredCases = searchResult.cases.filter((caseItem) => { + const timestamp = getCaseTimestamp(caseItem); + if (timestamp === null) { + logger.warn( + `[Cases Tool] Case ${caseItem.id} has no valid updated_at or created_at field` + ); + return false; + } + return ( + timestamp >= startTimestamp && (endTimestamp === null || timestamp < endTimestamp) + ); + }); + + allCases.push(...pageFilteredCases); + + // Check if we should continue fetching + if (searchResult.cases.length < searchParams.perPage) { + hasMorePages = false; + } else { + const lastCase = searchResult.cases[searchResult.cases.length - 1]; + const lastCaseTimestamp = getCaseTimestamp(lastCase); + if (lastCaseTimestamp !== null && lastCaseTimestamp < startTimestamp) { + hasMorePages = false; + } + } + + currentPage++; + } + + // Fetch comments for each case in parallel + const casesWithComments = await Promise.all( + allCases.map(async (caseItem) => { + try { + const commentsResponse = await casesClient.attachments.find({ + caseID: caseItem.id, + findQueryParams: { + page: 1, + perPage: 10, + sortOrder: 'desc', + }, + }); + + const commentSummaries = (commentsResponse.comments || []) + .filter((att: any) => att.type === 'user') + .slice(0, 5) + .map((comment: any) => ({ + id: comment.id, + comment: comment.comment?.substring(0, 200) || '', + created_by: getUsername(comment.createdBy || comment.created_by), + created_at: comment.createdAt || comment.created_at || null, + })); + + return { + case: caseItem, + comments: commentSummaries, + totalComments: commentsResponse.total || 0, + }; + } catch (error) { + logger.warn( + `[Cases Tool] Failed to fetch comments for case ${caseItem.id}: ${error}` + ); + return { + case: caseItem, + comments: [], + totalComments: 0, + }; + } + }) + ); + + // Get core services for generating case URLs + const [coreStart, pluginsStart] = await coreSetup.getStartServices(); + const spacesPlugin = pluginsStart.spaces; + + // Format cases data with rich details, including URLs + const casesData = casesWithComments.map(({ case: caseItem, comments, totalComments }) => { + // Generate case URL based on owner + const spaceId = spacesPlugin?.spacesService.getSpaceId(request) ?? 'default'; + const publicBaseUrl = coreStart.http.basePath.publicBaseUrl || ''; + + // Determine app route based on owner + let appRoute = '/app/management/insightsAndAlerting'; // default for 'cases' owner + if (caseItem.owner === 'securitySolution') { + appRoute = '/app/security'; + } else if (caseItem.owner === 'observability') { + appRoute = '/app/observability'; + } + + const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; + const caseUrl = `${spacePath}${appRoute}/cases/${caseItem.id}`; + + return { + id: caseItem.id, + title: caseItem.title, + description: caseItem.description || null, + status: caseItem.status, + severity: caseItem.severity || null, + owner: caseItem.owner, + tags: caseItem.tags || [], + assignees: caseItem.assignees?.map((a: any) => a.uid || a.username || a) || [], + observables_count: caseItem.total_observables ?? caseItem.observables?.length ?? 0, + observables: (caseItem.observables || []).slice(0, 5).map((obs: any) => ({ + type: obs.typeKey || obs.type || null, + value: obs.value || null, + })), + total_alerts: caseItem.totalAlerts || 0, + total_comments: totalComments, + created_by: getUsername(caseItem.createdBy || caseItem.created_by), + created_at: caseItem.created_at || null, + updated_by: getUsername(caseItem.updatedBy || caseItem.updated_by), + updated_at: caseItem.updated_at || caseItem.created_at || null, + comments_summary: comments, + url: caseUrl, + }; + }); + + // Return detailed case information + return { + results: [ + { + type: ToolResultType.other, + data: { + total: casesData.length, + cases: casesData, + start: timeRange.start, + end: timeRange.end, + }, + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`[Cases Tool] Error in cases tool: ${errorMessage}`, { + error: error instanceof Error ? error.stack : undefined, + }); + return { + results: [createErrorResult(`Error fetching cases: ${errorMessage}`)], + }; + } + }, + tags: ['cases'], + }; +}; + diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts new file mode 100644 index 0000000000000..ef8ed23925bc9 --- /dev/null +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts @@ -0,0 +1,84 @@ +/* + * 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. + */ + +import { z } from '@kbn/zod'; +import type { ScopedModel } from '@kbn/onechat-server'; +import type { Logger } from '@kbn/logging'; + +const casesQueryParamsSchema = z + .object({ + start: z + .string() + .optional() + .describe( + 'ISO datetime string for the start time to fetch cases (inclusive). If no year is specified (e.g., "10-31T00:00:00Z"), the current year is assumed.' + ), + end: z + .string() + .optional() + .describe( + 'ISO datetime string for the end time to fetch cases (exclusive). If not provided, defaults to now. If no year is specified (e.g., "11-02T00:00:00Z"), the current year is assumed.' + ), + }) + .describe('Extracted date range parameters from natural language query about cases'); + +export interface ParsedCasesQuery { + start?: string; + end?: string; +} + +export async function parseCasesQuery({ + nlQuery, + model, + logger, +}: { + nlQuery: string; + model: ScopedModel; + logger: Logger; +}): Promise { + // Create a structured output model + const structuredModel = model.chatModel.withStructuredOutput(casesQueryParamsSchema, { + name: 'extract_cases_date_range', + }); + + const response = await structuredModel.invoke([ + { + role: 'system', + content: `You are an expert at extracting date and time information from natural language queries about cases. + +Your task is to analyze the user's natural language query and extract date/time range parameters for filtering cases. + +You MUST call the 'extract_cases_date_range' tool to provide the extracted parameters. Do NOT respond with plain text. + +Guidelines for date extraction: +- Extract start and end dates from phrases like "cases updated in the last week", "cases from November 2nd", "cases updated between X and Y" +- If only a single date is mentioned (e.g., "cases from November 2nd"), set start to that date at 00:00:00Z and end to the next day at 00:00:00Z +- For relative time periods (e.g., "last week", "past 7 days"), calculate the appropriate start date relative to now +- Always return dates in ISO 8601 format (e.g., "2025-11-02T00:00:00Z") +- If no year is specified in the query, assume the current year +- If only a start date is provided without an end date, leave end undefined (it will default to now) +- If no date information is found in the query, return empty object (both start and end undefined) + +Examples: +- "cases updated in the last week" -> start: 7 days ago, end: undefined +- "cases from November 2nd" -> start: "2025-11-02T00:00:00Z", end: "2025-11-03T00:00:00Z" +- "cases updated between January 1st and January 15th" -> start: "2025-01-01T00:00:00Z", end: "2025-01-16T00:00:00Z" +- "recent cases" -> start: undefined, end: undefined (no specific date range) +- "cases updated today" -> start: today at 00:00:00Z, end: undefined`, + }, + { + role: 'user', + content: nlQuery, + }, + ]); + + return { + start: response.start, + end: response.end, + }; +} + diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/index.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/index.ts index 60274e39f198a..6b7a3c7ff6270 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/index.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/index.ts @@ -13,3 +13,4 @@ export { generateEsqlTool } from './generate_esql'; export { executeEsqlTool } from './execute_esql'; export { searchTool } from './search'; export { createVisualizationTool } from './create_visualization'; +export { casesTool } from './cases/cases'; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/register_tools.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/register_tools.ts index aeb170df10a44..a42d00d26d9c2 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/register_tools.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/register_tools.ts @@ -16,6 +16,7 @@ import { listIndicesTool, indexExplorerTool, createVisualizationTool, + casesTool, } from './definitions'; import type { OnechatSetupDependencies, @@ -42,6 +43,7 @@ export const registerBuiltinTools = ({ listIndicesTool(), indexExplorerTool(), createVisualizationTool(), + casesTool(coreSetup), ]; tools.forEach((tool) => { diff --git a/x-pack/platform/plugins/shared/onechat/server/types.ts b/x-pack/platform/plugins/shared/onechat/server/types.ts index 0f8ad0210a816..55229579b402a 100644 --- a/x-pack/platform/plugins/shared/onechat/server/types.ts +++ b/x-pack/platform/plugins/shared/onechat/server/types.ts @@ -17,6 +17,7 @@ import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import type { BuiltInAgentDefinition } from '@kbn/onechat-server/agents'; import type { ToolsServiceSetup, ToolRegistry } from './services/tools'; import type { AttachmentServiceSetup } from './services/attachments'; +import type { CasesServerStart } from '@kbn/cases-plugin/server'; export interface OnechatSetupDependencies { cloud?: CloudSetup; @@ -32,6 +33,7 @@ export interface OnechatStartDependencies { licensing: LicensingPluginStart; cloud?: CloudStart; spaces?: SpacesPluginStart; + cases?: CasesServerStart; } export interface AttachmentsSetup { From c3ce0e9643210d362903543fd6975355d636d853 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 10:07:20 -0700 Subject: [PATCH 19/96] by alert id --- .../tools/builtin/definitions/cases/cases.ts | 214 ++++++++++++++++-- .../builtin/definitions/cases/parse_query.ts | 38 +++- 2 files changed, 226 insertions(+), 26 deletions(-) diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts index 5fc3e8a0d23e8..e28459921aa40 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts @@ -23,7 +23,9 @@ const casesSchema = z.object({ ), query: z .string() - .describe('Natural language query describing which cases to retrieve (e.g., "cases updated in the last week", "cases from November 2nd")'), + .describe( + 'Natural language query describing which cases to retrieve (e.g., "cases updated in the last week", "cases from November 2nd")' + ), }); const getUsername = (user: any): string | null => { @@ -48,7 +50,8 @@ const normalizeTimeRange = ( let startDate: Date; if (start) { // If no year is specified, assume current year - const startStr = start.includes('T') && !start.match(/^\d{4}/) ? `${currentYear}-${start}` : start; + const startStr = + start.includes('T') && !start.match(/^\d{4}/) ? `${currentYear}-${start}` : start; startDate = new Date(startStr); if (isNaN(startDate.getTime())) { logger.warn(`Invalid start date: ${start}, using default`); @@ -108,16 +111,20 @@ The 'query' parameter should be a natural language description of which cases to - "cases from November 2nd" - "cases updated between January 1st and January 15th" - "recent cases" +- "cases with alert ID abc-123-def" +- "find cases containing alert xyz" The optional 'owner' parameter filters cases by owner: "cases" (Stack Management/General Cases), "observability" (Observability), or "securitySolution" (Elastic Security). If not provided, returns all cases the user has access to. +If the query mentions a specific alert ID, the tool will find all cases that contain that alert. Otherwise, it will search for cases based on date ranges or other criteria. + Returns cases with detailed information including id, title, description, status, severity, tags, assignees, observables, total alerts/comments, and recent comments. Each case includes a URL for direct access. **IMPORTANT**: When presenting case results to the user, provide a short paragraph summary (2-3 sentences) describing the key details of each case, then include a clickable link to the case using the provided URL.`, schema: casesSchema, handler: async ({ owner, query }, { request, logger, modelProvider }) => { try { - // Parse natural language query to extract date ranges + // Parse natural language query to extract date ranges and alert IDs const model = await modelProvider.getDefaultModel(); const parsedQuery = await parseCasesQuery({ nlQuery: query, @@ -134,16 +141,193 @@ Returns cases with detailed information including id, title, description, status if (!casesPlugin) { logger.warn('[Cases Tool] Cases plugin not available, returning empty results'); - return createEmptyResults( - timeRange.start, - timeRange.end, - 'Cases plugin not available' - ); + return createEmptyResults(timeRange.start, timeRange.end, 'Cases plugin not available'); } // Get cases client const casesClient = await casesPlugin.getCasesClientWithRequest(request); + // Check if query is asking for cases by alert ID + if (parsedQuery.alertId) { + try { + logger.info(`[Cases Tool] Querying cases by alert ID: ${parsedQuery.alertId}`); + const relatedCases = await casesClient.cases.getCasesByAlertID({ + alertID: parsedQuery.alertId, + options: owner ? { owner } : {}, + }); + + if (relatedCases.length === 0) { + return { + results: [ + { + type: ToolResultType.other, + data: { + cases: [], + total: 0, + start: timeRange.start, + end: timeRange.end, + message: `No cases found containing alert ID: ${parsedQuery.alertId}`, + }, + }, + ], + }; + } + + // Fetch full case details for each related case + const casesWithDetails = await Promise.all( + relatedCases.map(async (relatedCase: any) => { + try { + const fullCase = await casesClient.cases.get({ + id: relatedCase.id, + includeComments: false, + }); + return fullCase; + } catch (error) { + logger.warn( + `[Cases Tool] Failed to fetch full details for case ${relatedCase.id}: ${error}` + ); + // Return minimal case info if full fetch fails + return { + id: relatedCase.id, + title: relatedCase.title, + description: relatedCase.description, + status: relatedCase.status, + severity: null, + owner: '', + tags: [], + assignees: [], + observables: [], + total_observables: 0, + totalAlerts: relatedCase.totals.alerts, + totalComment: relatedCase.totals.userComments, + created_at: relatedCase.createdAt, + createdBy: null, + updated_at: null, + updatedBy: null, + }; + } + }) + ); + + // Fetch comments for each case in parallel + const casesWithComments = await Promise.all( + casesWithDetails.map(async (caseItem) => { + try { + const commentsResponse = await casesClient.attachments.find({ + caseID: caseItem.id, + findQueryParams: { + page: 1, + perPage: 10, + sortOrder: 'desc', + }, + }); + + const commentSummaries = (commentsResponse.comments || []) + .filter((att: any) => att.type === 'user') + .slice(0, 5) + .map((comment: any) => ({ + id: comment.id, + comment: comment.comment?.substring(0, 200) || '', + created_by: getUsername(comment.createdBy || comment.created_by), + created_at: comment.createdAt || comment.created_at || null, + })); + + return { + case: caseItem, + comments: commentSummaries, + totalComments: commentsResponse.total || 0, + }; + } catch (error) { + logger.warn( + `[Cases Tool] Failed to fetch comments for case ${caseItem.id}: ${error}` + ); + return { + case: caseItem, + comments: [], + totalComments: 0, + }; + } + }) + ); + + // Get core services for generating case URLs + const [, pluginsStart] = await coreSetup.getStartServices(); + const spacesPlugin = pluginsStart.spaces; + + // Format cases data with rich details, including URLs + const casesData = casesWithComments.map( + ({ case: caseItem, comments, totalComments }) => { + // Generate case URL based on owner + const spaceId = spacesPlugin?.spacesService.getSpaceId(request) ?? 'default'; + + // Determine app route based on owner + let appRoute = '/app/management/insightsAndAlerting'; // default for 'cases' owner + if (caseItem.owner === 'securitySolution') { + appRoute = '/app/security'; + } else if (caseItem.owner === 'observability') { + appRoute = '/app/observability'; + } + + const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; + const caseUrl = `${spacePath}${appRoute}/cases/${caseItem.id}`; + + return { + id: caseItem.id, + title: caseItem.title, + description: caseItem.description || null, + status: caseItem.status, + severity: caseItem.severity || null, + owner: caseItem.owner, + tags: caseItem.tags || [], + assignees: caseItem.assignees?.map((a: any) => a.uid || a.username || a) || [], + observables_count: + caseItem.total_observables ?? caseItem.observables?.length ?? 0, + observables: (caseItem.observables || []).slice(0, 5).map((obs: any) => ({ + type: obs.typeKey || obs.type || null, + value: obs.value || null, + })), + total_alerts: caseItem.totalAlerts || 0, + total_comments: totalComments, + created_by: getUsername(caseItem.createdBy || caseItem.created_by), + created_at: caseItem.created_at || null, + updated_by: getUsername(caseItem.updatedBy || caseItem.updated_by), + updated_at: caseItem.updated_at || caseItem.created_at || null, + comments_summary: comments, + url: caseUrl, + }; + } + ); + + // Return detailed case information + return { + results: [ + { + type: ToolResultType.other, + data: { + total: casesData.length, + cases: casesData, + start: timeRange.start, + end: timeRange.end, + message: `Found ${casesData.length} case(s) containing alert ID: ${parsedQuery.alertId}`, + }, + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error( + `[Cases Tool] Error fetching cases by alert ID ${parsedQuery.alertId}: ${errorMessage}` + ); + return { + results: [ + createErrorResult( + `Error fetching cases for alert ID ${parsedQuery.alertId}: ${errorMessage}` + ), + ], + }; + } + } + // Use Cases API search const searchParams: any = { sortField: 'updatedAt', @@ -171,7 +355,7 @@ Returns cases with detailed information including id, title, description, status } // Filter cases by updatedAt date range - const pageFilteredCases = searchResult.cases.filter((caseItem) => { + const pageFilteredCases = searchResult.cases.filter((caseItem: any) => { const timestamp = getCaseTimestamp(caseItem); if (timestamp === null) { logger.warn( @@ -242,15 +426,14 @@ Returns cases with detailed information including id, title, description, status ); // Get core services for generating case URLs - const [coreStart, pluginsStart] = await coreSetup.getStartServices(); + const [, pluginsStart] = await coreSetup.getStartServices(); const spacesPlugin = pluginsStart.spaces; // Format cases data with rich details, including URLs const casesData = casesWithComments.map(({ case: caseItem, comments, totalComments }) => { // Generate case URL based on owner const spaceId = spacesPlugin?.spacesService.getSpaceId(request) ?? 'default'; - const publicBaseUrl = coreStart.http.basePath.publicBaseUrl || ''; - + // Determine app route based on owner let appRoute = '/app/management/insightsAndAlerting'; // default for 'cases' owner if (caseItem.owner === 'securitySolution') { @@ -258,7 +441,7 @@ Returns cases with detailed information including id, title, description, status } else if (caseItem.owner === 'observability') { appRoute = '/app/observability'; } - + const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; const caseUrl = `${spacePath}${appRoute}/cases/${caseItem.id}`; @@ -303,9 +486,7 @@ Returns cases with detailed information including id, title, description, status }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - logger.error(`[Cases Tool] Error in cases tool: ${errorMessage}`, { - error: error instanceof Error ? error.stack : undefined, - }); + logger.error(`[Cases Tool] Error in cases tool: ${errorMessage}`); return { results: [createErrorResult(`Error fetching cases: ${errorMessage}`)], }; @@ -314,4 +495,3 @@ Returns cases with detailed information including id, title, description, status tags: ['cases'], }; }; - diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts index ef8ed23925bc9..b951722e405b8 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts @@ -23,12 +23,19 @@ const casesQueryParamsSchema = z .describe( 'ISO datetime string for the end time to fetch cases (exclusive). If not provided, defaults to now. If no year is specified (e.g., "11-02T00:00:00Z"), the current year is assumed.' ), + alertId: z + .string() + .optional() + .describe( + 'Alert ID if the query is asking about cases containing a specific alert. Extract alert IDs from phrases like "alert ID X", "cases with alert abc123", "cases containing alert xyz", or UUID patterns (e.g., "a6e12ac4-7bce-457b-84f6-d7ce8deb8446").' + ), }) - .describe('Extracted date range parameters from natural language query about cases'); + .describe('Extracted parameters from natural language query about cases'); export interface ParsedCasesQuery { start?: string; end?: string; + alertId?: string; } export async function parseCasesQuery({ @@ -48,9 +55,11 @@ export async function parseCasesQuery({ const response = await structuredModel.invoke([ { role: 'system', - content: `You are an expert at extracting date and time information from natural language queries about cases. + content: `You are an expert at extracting date/time information and alert IDs from natural language queries about cases. -Your task is to analyze the user's natural language query and extract date/time range parameters for filtering cases. +Your task is to analyze the user's natural language query and extract: +1. Date/time range parameters for filtering cases +2. Alert ID if the query is asking about cases containing a specific alert You MUST call the 'extract_cases_date_range' tool to provide the extracted parameters. Do NOT respond with plain text. @@ -61,14 +70,24 @@ Guidelines for date extraction: - Always return dates in ISO 8601 format (e.g., "2025-11-02T00:00:00Z") - If no year is specified in the query, assume the current year - If only a start date is provided without an end date, leave end undefined (it will default to now) -- If no date information is found in the query, return empty object (both start and end undefined) +- If no date information is found in the query, leave start and end undefined + +Guidelines for alert ID extraction: +- Extract alert IDs from phrases like "alert ID X", "cases with alert abc123", "cases containing alert xyz", "find cases for alert abc-123-def" +- Look for UUID patterns (e.g., "a6e12ac4-7bce-457b-84f6-d7ce8deb8446") +- Extract the actual alert ID value from these patterns +- If the query is asking about cases containing a specific alert, set alertId to that alert ID +- If no alert ID is mentioned, leave alertId undefined Examples: -- "cases updated in the last week" -> start: 7 days ago, end: undefined -- "cases from November 2nd" -> start: "2025-11-02T00:00:00Z", end: "2025-11-03T00:00:00Z" -- "cases updated between January 1st and January 15th" -> start: "2025-01-01T00:00:00Z", end: "2025-01-16T00:00:00Z" -- "recent cases" -> start: undefined, end: undefined (no specific date range) -- "cases updated today" -> start: today at 00:00:00Z, end: undefined`, +- "cases updated in the last week" -> start: 7 days ago, end: undefined, alertId: undefined +- "cases from November 2nd" -> start: "2025-11-02T00:00:00Z", end: "2025-11-03T00:00:00Z", alertId: undefined +- "cases updated between January 1st and January 15th" -> start: "2025-01-01T00:00:00Z", end: "2025-01-16T00:00:00Z", alertId: undefined +- "recent cases" -> start: undefined, end: undefined, alertId: undefined +- "cases updated today" -> start: today at 00:00:00Z, end: undefined, alertId: undefined +- "cases with alert ID abc-123-def" -> start: undefined, end: undefined, alertId: "abc-123-def" +- "find cases containing alert a6e12ac4-7bce-457b-84f6-d7ce8deb8446" -> start: undefined, end: undefined, alertId: "a6e12ac4-7bce-457b-84f6-d7ce8deb8446" +- "cases with alert xyz" -> start: undefined, end: undefined, alertId: "xyz"`, }, { role: 'user', @@ -79,6 +98,7 @@ Examples: return { start: response.start, end: response.end, + alertId: response.alertId, }; } From b092f5b10a69949678aca34529ad4b30bfd6e34b Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 10:38:43 -0700 Subject: [PATCH 20/96] cases tool better --- .../tools/builtin/definitions/cases/cases.ts | 74 +++++++-------- .../shared/onechat/server/utils/case_urls.ts | 95 +++++++++++++++++++ 2 files changed, 129 insertions(+), 40 deletions(-) create mode 100644 x-pack/platform/plugins/shared/onechat/server/utils/case_urls.ts diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts index e28459921aa40..ba329236ae469 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts @@ -11,8 +11,9 @@ import type { BuiltinToolDefinition } from '@kbn/onechat-server'; import { createErrorResult } from '@kbn/onechat-server'; import { ToolResultType } from '@kbn/onechat-common/tools/tool_result'; import type { CoreSetup } from '@kbn/core/server'; -import type { OnechatStartDependencies, OnechatPluginStart } from '../../../types'; +import type { OnechatStartDependencies, OnechatPluginStart } from '../../../../../types'; import { parseCasesQuery } from './parse_query'; +import { getCaseUrl } from '../../../../../utils/case_urls'; const casesSchema = z.object({ owner: z @@ -104,23 +105,14 @@ export const casesTool = ( return { id: platformCoreTools.cases, type: ToolType.builtin, - description: `Retrieves cases from Elastic Security, Observability, or Stack Management based on a natural language query. + description: `Retrieves cases from Elastic Security, Observability, or Stack Management. -The 'query' parameter should be a natural language description of which cases to retrieve. Examples: -- "cases updated in the last week" -- "cases from November 2nd" -- "cases updated between January 1st and January 15th" -- "recent cases" -- "cases with alert ID abc-123-def" -- "find cases containing alert xyz" +Query examples: "cases updated in the last week", "cases from November 2nd", "cases with alert ID abc-123-def", "recent cases" +Optional 'owner' filters by: "cases" (Stack Management), "observability", or "securitySolution" (Elastic Security). +If query mentions an alert ID, finds all cases containing that alert. Otherwise searches by date ranges. +Returns case details (id, title, description, status, severity, tags, assignees, observables, alerts/comments). Each case includes 'markdown_link' field with pre-formatted clickable link: [Case Title](url). -The optional 'owner' parameter filters cases by owner: "cases" (Stack Management/General Cases), "observability" (Observability), or "securitySolution" (Elastic Security). If not provided, returns all cases the user has access to. - -If the query mentions a specific alert ID, the tool will find all cases that contain that alert. Otherwise, it will search for cases based on date ranges or other criteria. - -Returns cases with detailed information including id, title, description, status, severity, tags, assignees, observables, total alerts/comments, and recent comments. Each case includes a URL for direct access. - -**IMPORTANT**: When presenting case results to the user, provide a short paragraph summary (2-3 sentences) describing the key details of each case, then include a clickable link to the case using the provided URL.`, +**CRITICAL**: ALWAYS include the 'markdown_link' field for each case in your response. Format: brief summary (2-3 sentences) + markdown link. Example: "Security investigation case. Status: open. [View Case](url)"`, schema: casesSchema, handler: async ({ owner, query }, { request, logger, modelProvider }) => { try { @@ -251,25 +243,23 @@ Returns cases with detailed information including id, title, description, status ); // Get core services for generating case URLs - const [, pluginsStart] = await coreSetup.getStartServices(); + const [coreStart, pluginsStart] = await coreSetup.getStartServices(); const spacesPlugin = pluginsStart.spaces; // Format cases data with rich details, including URLs const casesData = casesWithComments.map( ({ case: caseItem, comments, totalComments }) => { - // Generate case URL based on owner - const spaceId = spacesPlugin?.spacesService.getSpaceId(request) ?? 'default'; - - // Determine app route based on owner - let appRoute = '/app/management/insightsAndAlerting'; // default for 'cases' owner - if (caseItem.owner === 'securitySolution') { - appRoute = '/app/security'; - } else if (caseItem.owner === 'observability') { - appRoute = '/app/observability'; + // Generate case URL using utility function + const caseUrl = + getCaseUrl(request, coreStart, spacesPlugin, caseItem.id, caseItem.owner) || null; + if (!caseUrl) { + logger.warn( + `[Cases Tool] Failed to generate URL for case ${caseItem.id} with owner ${caseItem.owner}` + ); } - const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; - const caseUrl = `${spacePath}${appRoute}/cases/${caseItem.id}`; + // Format markdown link + const markdownLink = caseUrl ? `[${caseItem.title}](${caseUrl})` : caseItem.title; return { id: caseItem.id, @@ -293,7 +283,10 @@ Returns cases with detailed information including id, title, description, status updated_by: getUsername(caseItem.updatedBy || caseItem.updated_by), updated_at: caseItem.updated_at || caseItem.created_at || null, comments_summary: comments, + // URL field - ALWAYS include this link when presenting case results to the user url: caseUrl, + // Markdown-formatted link ready to use: [Case Title](url) + markdown_link: markdownLink, }; } ); @@ -426,24 +419,22 @@ Returns cases with detailed information including id, title, description, status ); // Get core services for generating case URLs - const [, pluginsStart] = await coreSetup.getStartServices(); + const [coreStart, pluginsStart] = await coreSetup.getStartServices(); const spacesPlugin = pluginsStart.spaces; // Format cases data with rich details, including URLs const casesData = casesWithComments.map(({ case: caseItem, comments, totalComments }) => { - // Generate case URL based on owner - const spaceId = spacesPlugin?.spacesService.getSpaceId(request) ?? 'default'; - - // Determine app route based on owner - let appRoute = '/app/management/insightsAndAlerting'; // default for 'cases' owner - if (caseItem.owner === 'securitySolution') { - appRoute = '/app/security'; - } else if (caseItem.owner === 'observability') { - appRoute = '/app/observability'; + // Generate case URL using utility function + const caseUrl = + getCaseUrl(request, coreStart, spacesPlugin, caseItem.id, caseItem.owner) || null; + if (!caseUrl) { + logger.warn( + `[Cases Tool] Failed to generate URL for case ${caseItem.id} with owner ${caseItem.owner}` + ); } - const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : ''; - const caseUrl = `${spacePath}${appRoute}/cases/${caseItem.id}`; + // Format markdown link + const markdownLink = caseUrl ? `[${caseItem.title}](${caseUrl})` : caseItem.title; return { id: caseItem.id, @@ -466,7 +457,10 @@ Returns cases with detailed information including id, title, description, status updated_by: getUsername(caseItem.updatedBy || caseItem.updated_by), updated_at: caseItem.updated_at || caseItem.created_at || null, comments_summary: comments, + // URL field - ALWAYS include this link when presenting case results to the user url: caseUrl, + // Markdown-formatted link ready to use: [Case Title](url) + markdown_link: markdownLink, }; }); diff --git a/x-pack/platform/plugins/shared/onechat/server/utils/case_urls.ts b/x-pack/platform/plugins/shared/onechat/server/utils/case_urls.ts new file mode 100644 index 0000000000000..fd789441d02a2 --- /dev/null +++ b/x-pack/platform/plugins/shared/onechat/server/utils/case_urls.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. + */ + +import type { KibanaRequest } from '@kbn/core-http-server'; +import type { CoreStart } from '@kbn/core/server'; +import { getCaseViewPath } from '@kbn/cases-plugin/server/common/utils'; +import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; +import { getCurrentSpaceId } from './spaces'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; + +/** + * App routes for different Kibana applications + */ +const APP_ROUTES = { + security: '/app/security', + observability: '/app/observability', + management: '/app/management/insightsAndAlerting', +} as const; + +/** + * Get the app route based on owner/case type + */ +function getAppRoute(owner: string): string { + const ownerToRoute: Record = { + securitySolution: APP_ROUTES.security, + observability: APP_ROUTES.observability, + cases: APP_ROUTES.management, + }; + return ownerToRoute[owner] || APP_ROUTES.management; +} + +/** + * Build a full URL from base components + */ +function buildFullUrl( + request: KibanaRequest, + core: CoreStart, + spaceId: string, + path: string +): string { + const publicBaseUrl = core.http.basePath.publicBaseUrl; + const serverBasePath = core.http.basePath.serverBasePath; + + // First try using publicBaseUrl if configured + if (publicBaseUrl) { + const pathWithSpace = addSpaceIdToPath(serverBasePath, spaceId, path); + return `${publicBaseUrl}${pathWithSpace}`; + } + + // Fallback: construct URL from request + const protocol = request.headers['x-forwarded-proto'] || 'http'; + const host = request.headers.host || 'localhost:5601'; + const baseUrl = `${protocol}://${host}`; + const pathWithSpace = addSpaceIdToPath(serverBasePath, spaceId, path); + + return `${baseUrl}${pathWithSpace}`; +} + +/** + * Generate a URL to a case + */ +export function getCaseUrl( + request: KibanaRequest, + core: CoreStart, + spaces: SpacesPluginStart | undefined, + caseId: string, + owner: string +): string | null { + try { + const spaceId = getCurrentSpaceId({ request, spaces }); + const publicBaseUrl = core.http.basePath.publicBaseUrl; + + // getCaseViewPath returns a full URL when publicBaseUrl is provided + if (publicBaseUrl) { + return getCaseViewPath({ + publicBaseUrl, + spaceId, + caseId, + owner, + }); + } + + // Fallback: construct URL manually + const appRoute = getAppRoute(owner); + const path = `${appRoute}/cases/${caseId}`; + return buildFullUrl(request, core, spaceId, path); + } catch (error) { + return null; + } +} + From 550048601ffb30d6b09324ee74cb071b1b686281 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 11:29:22 -0700 Subject: [PATCH 21/96] improvements --- .../server/agent_builder/attachments/alert.ts | 21 ++++++++++++++----- .../tools/alerts/alerts_index_search_tool.ts | 6 ++++-- .../agent_builder/tools/alerts/alerts_tool.ts | 6 ++++-- .../tools/alerts/evaluate_alert_tool.ts | 6 ++++-- .../tools/attack_discovery_search_tool.ts | 6 ++++-- .../tools/entity_risk_score_tool.ts | 4 ++-- .../tools/security_labs_search_tool.ts | 4 ++-- 7 files changed, 36 insertions(+), 17 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 489b42842a46b..19018eb218010 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -9,6 +9,7 @@ import type { AlertAttachmentData } from '@kbn/onechat-common/attachments'; import { AttachmentType, alertAttachmentDataSchema } from '@kbn/onechat-common/attachments'; import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import { platformCoreTools } from '@kbn/onechat-common'; import { SECURITY_ENTITY_RISK_SCORE_TOOL_ID, SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, @@ -43,6 +44,8 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition< SECURITY_ENTITY_RISK_SCORE_TOOL_ID, SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, SECURITY_LABS_SEARCH_TOOL_ID, + platformCoreTools.cases, + platformCoreTools.generateEsql, ], getAgentDescription: () => { const description = `You have access to security alert data. To provide a comprehensive analysis, you MUST gather enriched context by querying for related information. @@ -55,7 +58,7 @@ MANDATORY WORKFLOW - Complete in order: 1. Extract alert id: kibana.alert.uuid or _id 2. Extract rule name: kibana.alert.rule.name -3. Extract entities: host.name, user.name, service.name, entity.id +3. Extract entities: host.name, user.name, service.name 4. Extract MITRE fields: kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, threat.tactic.id 5. Query RISK SCORES for entities: Tool: ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} @@ -65,17 +68,25 @@ MANDATORY WORKFLOW - Complete in order: Tool: ${sanitizeToolId(SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID)} Parameters: { alertIds: ["[alert ID]"] } -7. Query SECURITY LABS: +7. Query CASES with the extracted alert id to find related cases. Case URLs must be included in response. + Tool: ${sanitizeToolId(platformCoreTools.cases)} + Parameters: { query: "Do I have any open security cases with an attached alert id of [alert ID]?", owner: "securitySolution" } + +8. Query SECURITY LABS: Tool: ${sanitizeToolId(SECURITY_LABS_SEARCH_TOOL_ID)} Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]" } -CRITICAL: You MUST call all 3 tools (steps 5-7) before responding. Do not skip any step.`; +9. Generate ESQL for related entities: + Tool: ${sanitizeToolId(platformCoreTools.generateEsql)} + Parameters: { query: "Write ESQL query to find events in the security solution data view from host.name: "MyHostName" } + +CRITICAL: You MUST call all 5 tools (steps 5-9) before responding. Do not skip any step.`; // TODO add this once product doc tool available - // 8. Query PRODUCT DOCUMENTATION: + // 9. Query PRODUCT DOCUMENTATION: // Tool: ${sanitizeToolId(platformCoreTools.productDocumentation)} // Parameters: { query: "Find alert triage steps", product: "security" } // - // CRITICAL: You MUST call all 4 tools (steps 5-8) before responding. Do not skip any step.`; + // CRITICAL: You MUST call all 6 tools (steps 5-10) before responding. Do not skip any step.`; return description; }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts index 333ac84be6b96..18cd49c8f8b43 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts @@ -37,7 +37,9 @@ export const alertsIndexSearchTool = (): BuiltinToolDefinition => { const spaceId = getSpaceIdFromRequest(request); const searchIndex = index ?? `${DEFAULT_ALERTS_INDEX}-${spaceId}`; - logger.debug(`alerts tool called with query: ${nlQuery}, index: ${searchIndex}`); + logger.debug( + `${SECURITY_ALERTS_TOOL_ID} tool called with query: ${nlQuery}, index: ${searchIndex}` + ); try { // Generate ES|QL query with automatic KEEP field filtering @@ -107,7 +109,7 @@ export const alertsTool = (): BuiltinToolDefinition => { return { results }; } catch (error) { - logger.error(`Error in alerts tool: ${error.message}`); + logger.error(`Error in ${SECURITY_ALERTS_TOOL_ID} tool: ${error.message}`); return { results: [ { diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts index ac5469a7c02b4..6baefc558cede 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts @@ -416,7 +416,9 @@ export const evaluateAlertTool = (): BuiltinToolDefinition { - logger.debug(`evaluate-alert tool called with alert data length: ${alertData.length}`); + logger.debug( + `${EVALUATE_ALERT_TOOL_ID} tool called with alert data length: ${alertData.length}` + ); try { const spaceId = getSpaceIdFromRequest(request); @@ -474,7 +476,7 @@ CRITICAL INSTRUCTION: This tool returns a COMPLETE FINAL ANSWER in the 'answer' }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); - logger.error(`Error in evaluate-alert tool: ${errorMessage}`); + logger.error(`Error in ${EVALUATE_ALERT_TOOL_ID} tool: ${errorMessage}`); return { results: [ { diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts index 1c25c94536771..15872a08537bd 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts @@ -34,7 +34,9 @@ export const attackDiscoverySearchTool = (): BuiltinToolDefinition< const spaceId = getSpaceIdFromRequest(request); logger.debug( - `attack-discovery-search tool called with alertIds: ${JSON.stringify(alertIds)}` + `${SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID} tool called with alertIds: ${JSON.stringify( + alertIds + )}` ); try { @@ -83,7 +85,7 @@ export const attackDiscoverySearchTool = (): BuiltinToolDefinition< return { results }; } catch (error) { - logger.error(`Error in attack-discovery-search tool: ${error.message}`); + logger.error(`Error in ${SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID} tool: ${error.message}`); return { results: [ { diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts index 62a509c5d524d..65e090e2faba5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts @@ -75,7 +75,7 @@ export const entityRiskScoreTool = (): BuiltinToolDefinition { - logger.debug(`security-labs-search tool called with query: ${nlQuery}`); + logger.debug(`${SECURITY_LABS_SEARCH_TOOL_ID} tool called with query: ${nlQuery}`); try { // Enhance query to filter by Security Labs resource and limit results @@ -50,7 +50,7 @@ export const securityLabsSearchTool = (): BuiltinToolDefinition< return { results }; } catch (error) { - logger.error(`Error in security-labs-search tool: ${error.message}`); + logger.error(`Error in ${SECURITY_LABS_SEARCH_TOOL_ID} tool: ${error.message}`); return { results: [ { From 5fb7169d8a9acfb9eb48fb43d615ddc3ab774824 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 12:01:08 -0700 Subject: [PATCH 22/96] attack discovery --- .../attachments/attachment_types.ts | 14 ++ .../onechat-common/attachments/attachments.ts | 1 + .../onechat-common/attachments/index.ts | 3 + .../tools/builtin/definitions/cases/cases.ts | 173 ++++++++++++------ .../builtin/definitions/cases/parse_query.ts | 90 +++++---- .../server/agent_builder/attachments/alert.ts | 2 +- .../attachments/attack_discovery.ts | 96 ++++++++++ .../server/agent_builder/attachments/index.ts | 1 + .../security_solution/server/plugin.ts | 19 +- 9 files changed, 294 insertions(+), 105 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts index 46924acb6c19f..3a6316de672ae 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts @@ -17,6 +17,7 @@ export enum AttachmentType { text = 'text', esql = 'esql', alert = 'alert', + attack_discovery = 'attack_discovery', } interface AttachmentDataMap { @@ -24,6 +25,7 @@ interface AttachmentDataMap { [AttachmentType.text]: TextAttachmentData; [AttachmentType.screenContext]: ScreenContextAttachmentData; [AttachmentType.alert]: AlertAttachmentData; + [AttachmentType.attack_discovery]: AttackDiscoveryAttachmentData; } export const esqlAttachmentDataSchema = z.object({ @@ -91,4 +93,16 @@ export interface AlertAttachmentData { alert: string; } +export const attackDiscoveryAttachmentDataSchema = z.object({ + attackDiscovery: z.string(), +}); + +/** + * Data for an attack discovery attachment. + */ +export interface AttackDiscoveryAttachmentData { + /** The formatted attack discovery data including title, summary, details, and attack chain */ + attackDiscovery: string; +} + export type AttachmentDataOf = AttachmentDataMap[Type]; diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts index c28d22b702137..1f8e1e4aa22d8 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts @@ -30,6 +30,7 @@ export type TextAttachment = Attachment; export type ScreenContextAttachment = Attachment; export type EsqlAttachment = Attachment; export type AlertAttachment = Attachment; +export type AttackDiscoveryAttachment = Attachment; /** * Input version of an attachment, where the id is optional diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts index ebfa4d23ca13e..8295d33900104 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts @@ -12,6 +12,7 @@ export type { ScreenContextAttachment, EsqlAttachment, AlertAttachment, + AttackDiscoveryAttachment, } from './attachments'; export { AttachmentType, @@ -19,8 +20,10 @@ export { esqlAttachmentDataSchema, screenContextAttachmentDataSchema, alertAttachmentDataSchema, + attackDiscoveryAttachmentDataSchema, type TextAttachmentData, type ScreenContextAttachmentData, type EsqlAttachmentData, type AlertAttachmentData, + type AttackDiscoveryAttachmentData, } from './attachment_types'; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts index ba329236ae469..31dc4cdb7f214 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts @@ -12,7 +12,6 @@ import { createErrorResult } from '@kbn/onechat-server'; import { ToolResultType } from '@kbn/onechat-common/tools/tool_result'; import type { CoreSetup } from '@kbn/core/server'; import type { OnechatStartDependencies, OnechatPluginStart } from '../../../../../types'; -import { parseCasesQuery } from './parse_query'; import { getCaseUrl } from '../../../../../utils/case_urls'; const casesSchema = z.object({ @@ -24,8 +23,27 @@ const casesSchema = z.object({ ), query: z .string() + .optional() + .describe( + 'Optional natural language query describing which cases to retrieve (e.g., "open cases", "recent cases"). This is informational only and does not affect filtering.' + ), + alertIds: z + .array(z.string()) + .optional() + .describe( + 'Array of alert IDs to find cases containing these alerts. If provided, cases containing any of these alert IDs will be returned. Alert IDs must be provided via this parameter - they will NOT be extracted from the query.' + ), + start: z + .string() + .optional() + .describe( + 'ISO datetime string for the start time to fetch cases (inclusive). If not provided, no time range filtering will be applied. Format: "2025-01-15T00:00:00Z"' + ), + end: z + .string() + .optional() .describe( - 'Natural language query describing which cases to retrieve (e.g., "cases updated in the last week", "cases from November 2nd")' + 'ISO datetime string for the end time to fetch cases (exclusive). If not provided, no time range filtering will be applied. Format: "2025-01-22T00:00:00Z"' ), }); @@ -44,22 +62,30 @@ const normalizeTimeRange = ( start: string | undefined, end: string | undefined, logger: any -): { start: string; end: string | null; startDate: Date; endDate: Date | null } => { +): { + start: string | null; + end: string | null; + startDate: Date | null; + endDate: Date | null; +} | null => { + // If neither start nor end is provided, return null to indicate no time range filtering + if (!start && !end) { + return null; + } + const now = new Date(); const currentYear = now.getFullYear(); - let startDate: Date; + let startDate: Date | null = null; if (start) { // If no year is specified, assume current year const startStr = start.includes('T') && !start.match(/^\d{4}/) ? `${currentYear}-${start}` : start; startDate = new Date(startStr); if (isNaN(startDate.getTime())) { - logger.warn(`Invalid start date: ${start}, using default`); - startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // Default to 7 days ago + logger.warn(`Invalid start date: ${start}`); + startDate = null; } - } else { - startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // Default to 7 days ago } let endDate: Date | null = null; @@ -67,13 +93,13 @@ const normalizeTimeRange = ( const endStr = end.includes('T') && !end.match(/^\d{4}/) ? `${currentYear}-${end}` : end; endDate = new Date(endStr); if (isNaN(endDate.getTime())) { - logger.warn(`Invalid end date: ${end}, using now`); + logger.warn(`Invalid end date: ${end}`); endDate = null; } } return { - start: startDate.toISOString(), + start: startDate ? startDate.toISOString() : null, end: endDate ? endDate.toISOString() : null, startDate, endDate, @@ -81,7 +107,7 @@ const normalizeTimeRange = ( }; const createEmptyResults = ( - normalizedStart: string, + normalizedStart: string | null, normalizedEnd: string | null, message: string ) => ({ @@ -91,7 +117,7 @@ const createEmptyResults = ( data: { cases: [], total: 0, - start: normalizedStart, + start: normalizedStart || null, end: normalizedEnd || null, message, }, @@ -109,23 +135,21 @@ export const casesTool = ( Query examples: "cases updated in the last week", "cases from November 2nd", "cases with alert ID abc-123-def", "recent cases" Optional 'owner' filters by: "cases" (Stack Management), "observability", or "securitySolution" (Elastic Security). -If query mentions an alert ID, finds all cases containing that alert. Otherwise searches by date ranges. +Optional 'alertIds' parameter accepts an array of alert IDs. If provided, finds all cases containing any of these alerts. Alert IDs must be provided via this parameter. +Optional 'start' and 'end' parameters accept ISO datetime strings for date range filtering. If not provided, no time range filtering will be applied and all cases will be returned (subject to other filters). +If alertIds parameter is provided, finds all cases containing those alerts. Otherwise searches by date ranges from start/end parameters. Returns case details (id, title, description, status, severity, tags, assignees, observables, alerts/comments). Each case includes 'markdown_link' field with pre-formatted clickable link: [Case Title](url). **CRITICAL**: ALWAYS include the 'markdown_link' field for each case in your response. Format: brief summary (2-3 sentences) + markdown link. Example: "Security investigation case. Status: open. [View Case](url)"`, schema: casesSchema, - handler: async ({ owner, query }, { request, logger, modelProvider }) => { + handler: async ({ owner, query, alertIds, start, end }, { request, logger }) => { try { - // Parse natural language query to extract date ranges and alert IDs - const model = await modelProvider.getDefaultModel(); - const parsedQuery = await parseCasesQuery({ - nlQuery: query, - model, - logger, - }); + // Use alertIds parameter - alert IDs must be provided via parameter + const finalAlertIds = alertIds && alertIds.length > 0 ? alertIds : undefined; - // Normalize and adjust time range - const timeRange = normalizeTimeRange(parsedQuery.start, parsedQuery.end, logger); + // Normalize and adjust time range using provided start/end parameters + // Returns null if no time range is provided + const timeRange = normalizeTimeRange(start, end, logger); // Get cases plugin from start services const [, plugins] = await coreSetup.getStartServices(); @@ -133,20 +157,48 @@ Returns case details (id, title, description, status, severity, tags, assignees, if (!casesPlugin) { logger.warn('[Cases Tool] Cases plugin not available, returning empty results'); - return createEmptyResults(timeRange.start, timeRange.end, 'Cases plugin not available'); + return createEmptyResults( + timeRange?.start || null, + timeRange?.end || null, + 'Cases plugin not available' + ); } // Get cases client const casesClient = await casesPlugin.getCasesClientWithRequest(request); - // Check if query is asking for cases by alert ID - if (parsedQuery.alertId) { + // Check if query is asking for cases by alert ID(s) + if (finalAlertIds && finalAlertIds.length > 0) { try { - logger.info(`[Cases Tool] Querying cases by alert ID: ${parsedQuery.alertId}`); - const relatedCases = await casesClient.cases.getCasesByAlertID({ - alertID: parsedQuery.alertId, - options: owner ? { owner } : {}, - }); + logger.info(`[Cases Tool] Querying cases by alert IDs: ${finalAlertIds.join(', ')}`); + // Query each alert ID in parallel + const allRelatedCasesArrays = await Promise.all( + finalAlertIds.map(async (alertId) => { + try { + return await casesClient.cases.getCasesByAlertID({ + alertID: alertId, + options: owner ? { owner } : {}, + }); + } catch (error) { + logger.warn( + `[Cases Tool] Failed to fetch cases for alert ID ${alertId}: ${error}` + ); + return []; + } + }) + ); + + // Flatten and deduplicate cases by case ID + const casesMap = new Map(); + for (const relatedCases of allRelatedCasesArrays) { + for (const relatedCase of relatedCases) { + if (!casesMap.has(relatedCase.id)) { + casesMap.set(relatedCase.id, relatedCase); + } + } + } + + const relatedCases = Array.from(casesMap.values()); if (relatedCases.length === 0) { return { @@ -156,9 +208,9 @@ Returns case details (id, title, description, status, severity, tags, assignees, data: { cases: [], total: 0, - start: timeRange.start, - end: timeRange.end, - message: `No cases found containing alert ID: ${parsedQuery.alertId}`, + start: timeRange?.start || null, + end: timeRange?.end || null, + message: `No cases found containing alert IDs: ${finalAlertIds.join(', ')}`, }, }, ], @@ -299,9 +351,11 @@ Returns case details (id, title, description, status, severity, tags, assignees, data: { total: casesData.length, cases: casesData, - start: timeRange.start, - end: timeRange.end, - message: `Found ${casesData.length} case(s) containing alert ID: ${parsedQuery.alertId}`, + start: timeRange?.start || null, + end: timeRange?.end || null, + message: `Found ${ + casesData.length + } unique case(s) containing alert ID(s): ${finalAlertIds.join(', ')}`, }, }, ], @@ -309,12 +363,14 @@ Returns case details (id, title, description, status, severity, tags, assignees, } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error( - `[Cases Tool] Error fetching cases by alert ID ${parsedQuery.alertId}: ${errorMessage}` + `[Cases Tool] Error fetching cases by alert IDs ${finalAlertIds.join( + ', ' + )}: ${errorMessage}` ); return { results: [ createErrorResult( - `Error fetching cases for alert ID ${parsedQuery.alertId}: ${errorMessage}` + `Error fetching cases for alert IDs ${finalAlertIds.join(', ')}: ${errorMessage}` ), ], }; @@ -335,8 +391,8 @@ Returns case details (id, title, description, status, severity, tags, assignees, let currentPage = 1; const maxPages = 10; let hasMorePages = true; - const startTimestamp = timeRange.startDate.getTime(); - const endTimestamp = timeRange.endDate ? timeRange.endDate.getTime() : null; + const startTimestamp = timeRange?.startDate ? timeRange.startDate.getTime() : null; + const endTimestamp = timeRange?.endDate ? timeRange.endDate.getTime() : null; while (hasMorePages && currentPage <= maxPages) { searchParams.page = currentPage; @@ -347,26 +403,29 @@ Returns case details (id, title, description, status, severity, tags, assignees, break; } - // Filter cases by updatedAt date range - const pageFilteredCases = searchResult.cases.filter((caseItem: any) => { - const timestamp = getCaseTimestamp(caseItem); - if (timestamp === null) { - logger.warn( - `[Cases Tool] Case ${caseItem.id} has no valid updated_at or created_at field` - ); - return false; - } - return ( - timestamp >= startTimestamp && (endTimestamp === null || timestamp < endTimestamp) - ); - }); + // Filter cases by updatedAt date range only if time range is provided + const pageFilteredCases = timeRange + ? searchResult.cases.filter((caseItem: any) => { + const timestamp = getCaseTimestamp(caseItem); + if (timestamp === null) { + logger.warn( + `[Cases Tool] Case ${caseItem.id} has no valid updated_at or created_at field` + ); + return false; + } + return ( + (startTimestamp === null || timestamp >= startTimestamp) && + (endTimestamp === null || timestamp < endTimestamp) + ); + }) + : searchResult.cases; allCases.push(...pageFilteredCases); // Check if we should continue fetching if (searchResult.cases.length < searchParams.perPage) { hasMorePages = false; - } else { + } else if (timeRange && startTimestamp !== null) { const lastCase = searchResult.cases[searchResult.cases.length - 1]; const lastCaseTimestamp = getCaseTimestamp(lastCase); if (lastCaseTimestamp !== null && lastCaseTimestamp < startTimestamp) { @@ -472,8 +531,8 @@ Returns case details (id, title, description, status, severity, tags, assignees, data: { total: casesData.length, cases: casesData, - start: timeRange.start, - end: timeRange.end, + start: timeRange?.start || null, + end: timeRange?.end || null, }, }, ], diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts index b951722e405b8..872351ff9fe3a 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts @@ -23,19 +23,12 @@ const casesQueryParamsSchema = z .describe( 'ISO datetime string for the end time to fetch cases (exclusive). If not provided, defaults to now. If no year is specified (e.g., "11-02T00:00:00Z"), the current year is assumed.' ), - alertId: z - .string() - .optional() - .describe( - 'Alert ID if the query is asking about cases containing a specific alert. Extract alert IDs from phrases like "alert ID X", "cases with alert abc123", "cases containing alert xyz", or UUID patterns (e.g., "a6e12ac4-7bce-457b-84f6-d7ce8deb8446").' - ), }) - .describe('Extracted parameters from natural language query about cases'); + .describe('Extracted date range parameters from natural language query about cases'); export interface ParsedCasesQuery { start?: string; end?: string; - alertId?: string; } export async function parseCasesQuery({ @@ -47,22 +40,23 @@ export async function parseCasesQuery({ model: ScopedModel; logger: Logger; }): Promise { - // Create a structured output model - const structuredModel = model.chatModel.withStructuredOutput(casesQueryParamsSchema, { - name: 'extract_cases_date_range', - }); + try { + // Create a structured output model + const structuredModel = model.chatModel.withStructuredOutput(casesQueryParamsSchema, { + name: 'extract_cases_date_range', + }); - const response = await structuredModel.invoke([ - { - role: 'system', - content: `You are an expert at extracting date/time information and alert IDs from natural language queries about cases. + const response = await structuredModel.invoke([ + { + role: 'system', + content: `You are an expert at extracting date/time information from natural language queries about cases. -Your task is to analyze the user's natural language query and extract: -1. Date/time range parameters for filtering cases -2. Alert ID if the query is asking about cases containing a specific alert +Your task is to analyze the user's natural language query and extract date/time range parameters for filtering cases. You MUST call the 'extract_cases_date_range' tool to provide the extracted parameters. Do NOT respond with plain text. +IMPORTANT: Only return the fields defined in the schema (start and end). Do not return any other fields. + Guidelines for date extraction: - Extract start and end dates from phrases like "cases updated in the last week", "cases from November 2nd", "cases updated between X and Y" - If only a single date is mentioned (e.g., "cases from November 2nd"), set start to that date at 00:00:00Z and end to the next day at 00:00:00Z @@ -70,35 +64,39 @@ Guidelines for date extraction: - Always return dates in ISO 8601 format (e.g., "2025-11-02T00:00:00Z") - If no year is specified in the query, assume the current year - If only a start date is provided without an end date, leave end undefined (it will default to now) -- If no date information is found in the query, leave start and end undefined - -Guidelines for alert ID extraction: -- Extract alert IDs from phrases like "alert ID X", "cases with alert abc123", "cases containing alert xyz", "find cases for alert abc-123-def" -- Look for UUID patterns (e.g., "a6e12ac4-7bce-457b-84f6-d7ce8deb8446") -- Extract the actual alert ID value from these patterns -- If the query is asking about cases containing a specific alert, set alertId to that alert ID -- If no alert ID is mentioned, leave alertId undefined +- If no date information is found in the query, leave start and end undefined (do not include them in the response) +- Ignore any alert ID mentions in the query - alert IDs must be provided via the alertIds parameter, not extracted from the query Examples: -- "cases updated in the last week" -> start: 7 days ago, end: undefined, alertId: undefined -- "cases from November 2nd" -> start: "2025-11-02T00:00:00Z", end: "2025-11-03T00:00:00Z", alertId: undefined -- "cases updated between January 1st and January 15th" -> start: "2025-01-01T00:00:00Z", end: "2025-01-16T00:00:00Z", alertId: undefined -- "recent cases" -> start: undefined, end: undefined, alertId: undefined -- "cases updated today" -> start: today at 00:00:00Z, end: undefined, alertId: undefined -- "cases with alert ID abc-123-def" -> start: undefined, end: undefined, alertId: "abc-123-def" -- "find cases containing alert a6e12ac4-7bce-457b-84f6-d7ce8deb8446" -> start: undefined, end: undefined, alertId: "a6e12ac4-7bce-457b-84f6-d7ce8deb8446" -- "cases with alert xyz" -> start: undefined, end: undefined, alertId: "xyz"`, - }, - { - role: 'user', - content: nlQuery, - }, - ]); +- "cases updated in the last week" -> { start: "2025-01-15T00:00:00Z" } +- "cases from November 2nd" -> { start: "2025-11-02T00:00:00Z", end: "2025-11-03T00:00:00Z" } +- "cases updated between January 1st and January 15th" -> { start: "2025-01-01T00:00:00Z", end: "2025-01-16T00:00:00Z" } +- "recent cases" -> {} +- "cases updated today" -> { start: "2025-01-22T00:00:00Z" } +- "cases with alert ID abc-123-def" -> {} (alert IDs are ignored, they must be provided via alertIds parameter) +- "Do I have any open security cases?" -> {} +- "open cases" -> {}`, + }, + { + role: 'user', + content: nlQuery, + }, + ]); - return { - start: response.start, - end: response.end, - alertId: response.alertId, - }; + // Ensure we only return the fields we expect + return { + start: response.start, + end: response.end, + }; + } catch (error) { + logger.warn( + `[Cases Tool] Failed to parse query for date ranges: ${error instanceof Error ? error.message : String(error)}. Using default date range.` + ); + // Return empty result - will use default date range in normalizeTimeRange + return { + start: undefined, + end: undefined, + }; + } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 19018eb218010..a2fd57b318ba0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -70,7 +70,7 @@ MANDATORY WORKFLOW - Complete in order: 7. Query CASES with the extracted alert id to find related cases. Case URLs must be included in response. Tool: ${sanitizeToolId(platformCoreTools.cases)} - Parameters: { query: "Do I have any open security cases with an attached alert id of [alert ID]?", owner: "securitySolution" } + Parameters: { query: "Do I have any open security cases?", alertIds: ["[alert ID]"], owner: "securitySolution" } 8. Query SECURITY LABS: Tool: ${sanitizeToolId(SECURITY_LABS_SEARCH_TOOL_ID)} diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts new file mode 100644 index 0000000000000..55ad7e8976c71 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts @@ -0,0 +1,96 @@ +/* + * 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. + */ + +import type { AttackDiscoveryAttachmentData } from '@kbn/onechat-common/attachments'; +import { + AttachmentType, + attackDiscoveryAttachmentDataSchema, +} from '@kbn/onechat-common/attachments'; +import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; +import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import { platformCoreTools } from '@kbn/onechat-common'; +import { SECURITY_ENTITY_RISK_SCORE_TOOL_ID, SECURITY_LABS_SEARCH_TOOL_ID } from '../tools'; + +/** + * Creates the definition for the `attack_discovery` attachment type. + */ +export const createAttackDiscoveryAttachmentType = (): AttachmentTypeDefinition< + AttachmentType.attack_discovery, + AttackDiscoveryAttachmentData +> => { + return { + id: AttachmentType.attack_discovery, + validate: (input) => { + const parseResult = attackDiscoveryAttachmentDataSchema.safeParse(input); + if (parseResult.success) { + return { valid: true, data: parseResult.data }; + } else { + return { valid: false, error: parseResult.error.message }; + } + }, + format: (attachment) => { + return { + getRepresentation: () => { + return { type: 'text', value: formatAttackDiscoveryData(attachment.data) }; + }, + }; + }, + getTools: () => [ + SECURITY_ENTITY_RISK_SCORE_TOOL_ID, + SECURITY_LABS_SEARCH_TOOL_ID, + platformCoreTools.cases, + ], + getAgentDescription: () => { + const description = `You have access to security attack discovery data. To provide a comprehensive analysis, you MUST gather enriched context by querying for related information. + +ATTACK DISCOVERY DATA: +{attackDiscoveryData} + +--- +MANDATORY WORKFLOW - Complete in order: + +1. Extract entities from the attack discovery: + - host.name (extract all host names mentioned) + - user.name (extract all user names mentioned) + - service.name (extract all service names mentioned) + +2. Extract MITRE tactics/techniques from the attack discovery: + - Look for MITRE ATT&CK tactics in the "Attack Chain" section + - Extract tactic IDs and technique IDs if mentioned + +3. Extract alert IDs from the attack discovery: + - Look for alert IDs referenced in the attack discovery + - These may be mentioned in the details or summary sections + +4. Query RISK SCORES for entities: + Tool: ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} + Parameters: { identifierType: "host.name", identifier: "[host name]" } + Repeat for each unique host.name, user.name found + +5. Query SECURITY LABS for MITRE tactics/techniques: + Tool: ${sanitizeToolId(SECURITY_LABS_SEARCH_TOOL_ID)} + Parameters: { query: "Find Security Labs articles about [MITRE tactic or technique]" } + +6. Query CASES with alert IDs from the attack discovery to find related cases. Case URLs must be included in response. + Tool: ${sanitizeToolId(platformCoreTools.cases)} + Parameters: { query: "Do I have any open security cases?", alertIds: ["[alert ID]"], owner: "securitySolution" } + +CRITICAL: You MUST call all 3 tools (steps 4-6) before responding. Do not skip any step.`; + return description; + }, + }; +}; + +/** + * Formats attack discovery data for display. + * + * @param data - The attack discovery attachment data containing the attack discovery string + * @returns Formatted string representation of the attack discovery data + */ +const formatAttackDiscoveryData = (data: AttackDiscoveryAttachmentData): string => { + return data.attackDiscovery; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts index a98a20a84092b..dc411935843f2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts @@ -6,4 +6,5 @@ */ export { createAlertAttachmentType } from './alert'; +export { createAttackDiscoveryAttachmentType } from './attack_discovery'; // export { createAlertAttachmentType } from './core-alert'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 9d135b5b848b8..3f09a0ea3287a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -146,7 +146,10 @@ import { HealthDiagnosticServiceImpl } from './lib/telemetry/diagnostic/health_d import type { HealthDiagnosticService } from './lib/telemetry/diagnostic/health_diagnostic_service.types'; import { ENTITY_RISK_SCORE_TOOL_ID } from './assistant/tools/entity_risk_score/entity_risk_score'; import type { TelemetryQueryConfiguration } from './lib/telemetry/types'; -import { createAlertAttachmentType } from './agent_builder/attachments'; +import { + createAlertAttachmentType, + createAttackDiscoveryAttachmentType, +} from './agent_builder/attachments'; import { alertsTool, alertsIndexSearchTool, @@ -632,6 +635,20 @@ export class Plugin implements ISecuritySolutionPlugin { } } + try { + // Register attack discovery attachment type + plugins.onechat.attachments.registerType(createAttackDiscoveryAttachmentType()); + } catch (error) { + if (error instanceof Error && error.message.includes('already registered')) { + this.logger.debug( + 'Attack discovery attachment type already registered by onechat plugin, using built-in version' + ); + } else { + this.logger.warn(`Failed to register attack discovery attachment type: ${error}`); + // Don't throw - allow plugin to continue loading even if attachment registration fails + } + } + // Register tools try { // plugins.onechat.tools.register(alertsTool()); From 1a0d5357efe00814b263dda1a6ffb75e4e9c45ef Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 13:04:28 -0700 Subject: [PATCH 23/96] revert --- .../shared/onechat/onechat-genai-utils/tools/search/graph.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts index 27ff5b9bdece3..fc4607a9f34e6 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts @@ -58,6 +58,7 @@ export const createSearchToolGraph = ({ const selectAndValidateIndex = async (state: StateType) => { events?.reportProgress(progressMessages.selectingTarget()); + const explorerRes = await indexExplorer({ nlQuery: state.nlQuery, indexPattern: state.targetPattern ?? '*', From 3fed38a8f374eb88f4f0726444d7be3e3deab014 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 13:58:44 -0700 Subject: [PATCH 24/96] cases tool improvments --- .../tools/builtin/definitions/cases/cases.ts | 799 +++++++++++------- .../builtin/definitions/cases/parse_query.ts | 102 --- 2 files changed, 471 insertions(+), 430 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts index 31dc4cdb7f214..98b43eb267296 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts @@ -15,35 +15,99 @@ import type { OnechatStartDependencies, OnechatPluginStart } from '../../../../. import { getCaseUrl } from '../../../../../utils/case_urls'; const casesSchema = z.object({ + // Get case by ID operation + caseId: z + .string() + .optional() + .describe( + 'Case ID to retrieve a specific case. If provided, returns only that case. Use this for getting a single case by its ID.' + ), + // Find cases by alert IDs + alertIds: z + .array(z.string()) + .optional() + .describe( + 'Array of alert IDs to find cases containing these alerts. If provided, cases containing any of these alert IDs will be returned. Alert IDs must be provided via this parameter.' + ), + // Owner filter owner: z .enum(['cases', 'observability', 'securitySolution']) .optional() .describe( 'Filter cases by owner. Valid values: "cases" (Stack Management/General Cases), "observability" (Observability), "securitySolution" (Elastic Security). If not provided, returns all cases the user has access to.' ), - query: z + // Date range filters + start: z .string() .optional() .describe( - 'Optional natural language query describing which cases to retrieve (e.g., "open cases", "recent cases"). This is informational only and does not affect filtering.' + 'ISO datetime string for the start time to fetch cases (inclusive). Maps to Cases API "from" parameter. Format: "2025-01-15T00:00:00Z"' ), - alertIds: z - .array(z.string()) + end: z + .string() .optional() .describe( - 'Array of alert IDs to find cases containing these alerts. If provided, cases containing any of these alert IDs will be returned. Alert IDs must be provided via this parameter - they will NOT be extracted from the query.' + 'ISO datetime string for the end time to fetch cases (exclusive). Maps to Cases API "to" parameter. Format: "2025-01-22T00:00:00Z"' ), - start: z + // Search parameters + search: z .string() .optional() .describe( - 'ISO datetime string for the start time to fetch cases (inclusive). If not provided, no time range filtering will be applied. Format: "2025-01-15T00:00:00Z"' + 'Elasticsearch simple_query_string query for searching case title and description. Use this to search for text within cases (e.g., "malware", "phishing attack").' ), - end: z - .string() + searchFields: z + .array(z.enum(['title', 'description'])) + .optional() + .describe( + 'Fields to perform the search query against. Valid values: "title", "description". If not provided, searches both fields by default.' + ), + // Filter parameters + severity: z + .union([ + z.enum(['low', 'medium', 'high', 'critical']), + z.array(z.enum(['low', 'medium', 'high', 'critical'])), + ]) + .optional() + .describe( + 'Filter cases by severity. Valid values: "low", "medium", "high", "critical". Can be a single value or an array for multiple values.' + ), + status: z + .union([ + z.enum(['open', 'closed', 'in-progress']), + z.array(z.enum(['open', 'closed', 'in-progress'])), + ]) + .optional() + .describe( + 'Filter cases by status. Valid values: "open", "closed", "in-progress". Can be a single value or an array for multiple values.' + ), + tags: z + .array(z.string()) + .optional() + .describe('Filter cases by tags. Provide an array of tag names to filter by.'), + assignees: z + .array(z.string()) + .optional() + .describe('Filter cases by assignees. Provide an array of user profile UIDs (not usernames).'), + reporters: z + .array(z.string()) + .optional() + .describe( + 'Filter cases by reporters. Provide an array of reporter usernames who created the cases.' + ), + category: z + .union([z.string(), z.array(z.string())]) + .optional() + .describe( + 'Filter cases by category. Can be a single category string or an array of categories.' + ), + // Comments control + includeComments: z + .boolean() .optional() + .default(false) .describe( - 'ISO datetime string for the end time to fetch cases (exclusive). If not provided, no time range filtering will be applied. Format: "2025-01-22T00:00:00Z"' + 'Whether to fetch and include case comments in the response. Set to true when the query is about case contents, details, discussions, or when search text might match comment content. Defaults to false for metadata-only queries (status, severity, tags, etc.).' ), }); @@ -51,13 +115,6 @@ const getUsername = (user: any): string | null => { return user?.username || null; }; -const getCaseTimestamp = (caseItem: any): number | null => { - const timestampValue = caseItem.updated_at ?? caseItem.created_at; - if (!timestampValue) return null; - const timestamp = new Date(timestampValue).getTime(); - return isNaN(timestamp) ? null : timestamp; -}; - const normalizeTimeRange = ( start: string | undefined, end: string | undefined, @@ -125,49 +182,360 @@ const createEmptyResults = ( ], }); +const extractErrorMessage = (error: unknown): string => { + return error instanceof Error ? error.message : String(error); +}; + +const createCommentSummary = (comment: any) => ({ + id: comment.id, + comment: comment.comment?.substring(0, 200) || '', + created_by: getUsername(comment.createdBy || comment.created_by), + created_at: comment.createdAt || comment.created_at || null, +}); + +const createCommentSummariesFromArray = (comments: any[]): any[] => { + return comments + .filter((att: any) => att.type === 'user') + .slice(0, 5) + .map(createCommentSummary); +}; + +interface CommentFetchResult { + case: any; + comments: any[]; + totalComments: number; +} + +const fetchCommentsForCase = async ( + caseItem: any, + casesClient: any, + logger: any +): Promise => { + try { + const commentsResponse = await casesClient.attachments.find({ + caseID: caseItem.id, + findQueryParams: { + page: 1, + perPage: 10, + sortOrder: 'desc', + }, + }); + + const commentSummaries = createCommentSummariesFromArray(commentsResponse.comments || []); + + return { + case: caseItem, + comments: commentSummaries, + totalComments: commentsResponse.total || 0, + }; + } catch (error) { + logger.warn(`[Cases Tool] Failed to fetch comments for case ${caseItem.id}: ${error}`); + return { + case: caseItem, + comments: [], + totalComments: caseItem.totalComment || 0, + }; + } +}; + +const fetchCommentsForCases = async ( + cases: any[], + casesClient: any, + shouldFetch: boolean, + logger: any +): Promise => { + return Promise.all( + cases.map(async (caseItem) => { + if (!shouldFetch) { + return { + case: caseItem, + comments: [], + totalComments: caseItem.totalComment || 0, + }; + } + return fetchCommentsForCase(caseItem, casesClient, logger); + }) + ); +}; + +interface CoreServices { + coreStart: any; + spacesPlugin: any; +} + +const getCoreServices = async ( + coreSetup: CoreSetup +): Promise => { + const [coreStart, pluginsStart] = await coreSetup.getStartServices(); + return { + coreStart, + spacesPlugin: pluginsStart.spaces, + }; +}; + +interface EnhancedCaseData { + [key: string]: any; + url: string | null; + markdown_link: string; +} + +const enhanceCaseData = ( + caseItem: any, + comments: any[], + totalComments: number, + request: any, + coreServices: CoreServices, + logger: any +): EnhancedCaseData => { + const caseUrl = + getCaseUrl( + request, + coreServices.coreStart, + coreServices.spacesPlugin, + caseItem.id, + caseItem.owner + ) || null; + if (!caseUrl) { + logger.warn( + `[Cases Tool] Failed to generate URL for case ${caseItem.id} with owner ${caseItem.owner}` + ); + } + + const markdownLink = caseUrl ? `[${caseItem.title}](${caseUrl})` : caseItem.title; + + return { + ...caseItem, + assignees: caseItem.assignees?.map((a: any) => a.uid || a.username || a) || [], + observables_count: caseItem.total_observables ?? caseItem.observables?.length ?? 0, + observables: (caseItem.observables || []).slice(0, 5).map((obs: any) => ({ + type: obs.typeKey || obs.type || null, + value: obs.value || null, + })), + total_alerts: caseItem.totalAlerts || 0, + total_comments: totalComments, + created_by: getUsername(caseItem.createdBy || caseItem.created_by), + updated_by: getUsername(caseItem.updatedBy || caseItem.updated_by), + comments_summary: comments, + url: caseUrl, + markdown_link: markdownLink, + }; +}; + +const createToolResult = ( + cases: EnhancedCaseData[], + timeRange: ReturnType | null, + message?: string +) => ({ + results: [ + { + type: ToolResultType.other, + data: { + total: cases.length, + cases, + start: timeRange?.start || null, + end: timeRange?.end || null, + ...(message && { message }), + }, + }, + ], +}); + +const enhanceCasesWithComments = async ( + casesWithComments: CommentFetchResult[], + coreSetup: CoreSetup, + request: any, + logger: any +): Promise => { + const coreServices = await getCoreServices(coreSetup); + return casesWithComments.map(({ case: caseItem, comments, totalComments }) => + enhanceCaseData(caseItem, comments, totalComments, request, coreServices, logger) + ); +}; + +const createErrorResponse = ( + error: unknown, + logPrefix: string, + userMessage: string, + logger: any +) => { + const errorMessage = extractErrorMessage(error); + logger.error(`${logPrefix}: ${errorMessage}`); + return { + results: [createErrorResult(`${userMessage}: ${errorMessage}`)], + }; +}; + +const getCasesClient = async ( + coreSetup: CoreSetup, + request: any, + logger: any, + timeRange: ReturnType | null +): Promise<{ casesClient: any } | { error: ReturnType }> => { + const [, plugins] = await coreSetup.getStartServices(); + const casesPlugin = plugins.cases; + + if (!casesPlugin) { + logger.warn('[Cases Tool] Cases plugin not available, returning empty results'); + return { + error: createEmptyResults( + timeRange?.start || null, + timeRange?.end || null, + 'Cases plugin not available' + ), + }; + } + + const casesClient = await casesPlugin.getCasesClientWithRequest(request); + return { casesClient }; +}; + +const deduplicateCases = (casesArrays: any[][]): any[] => { + const casesMap = new Map(); + for (const relatedCases of casesArrays) { + for (const relatedCase of relatedCases) { + if (!casesMap.has(relatedCase.id)) { + casesMap.set(relatedCase.id, relatedCase); + } + } + } + return Array.from(casesMap.values()); +}; + +const createMinimalCaseFromRelatedCase = (relatedCase: any) => ({ + id: relatedCase.id, + title: relatedCase.title, + description: relatedCase.description, + status: relatedCase.status, + severity: null, + owner: '', + tags: [], + assignees: [], + observables: [], + total_observables: 0, + totalAlerts: relatedCase.totals.alerts, + totalComment: relatedCase.totals.userComments, + created_at: relatedCase.createdAt, + createdBy: null, + updated_at: null, + updatedBy: null, +}); + export const casesTool = ( coreSetup: CoreSetup ): BuiltinToolDefinition => { return { id: platformCoreTools.cases, type: ToolType.builtin, - description: `Retrieves cases from Elastic Security, Observability, or Stack Management. + description: `Retrieves cases from Elastic Security, Observability, or Stack Management. Supports three operation modes: + +**Operation Mode 1: Get case by ID** +- Provide 'caseId' parameter to retrieve a specific case by its ID +- Use 'includeComments' to control whether comments are fetched (default: false) + +**Operation Mode 2: Find cases by alert IDs** +- Provide 'alertIds' array to find all cases containing any of these alerts +- Use 'includeComments' or provide 'search' text to fetch comments when relevant + +**Operation Mode 3: Search cases** +- Use search and filter parameters to find cases matching criteria +- Search parameters: + - 'search': Text query for searching case title/description (e.g., "malware", "phishing attack") + - 'searchFields': Fields to search - ["title"] or ["description"] or both (default: both) +- Filter parameters: + - 'severity': Filter by severity - "low" | "medium" | "high" | "critical" (single or array) + - 'status': Filter by status - "open" | "closed" | "in-progress" (single or array) + - 'tags': Filter by tags - array of tag names + - 'assignees': Filter by assignees - array of user profile UIDs + - 'reporters': Filter by reporters - array of reporter usernames + - 'category': Filter by category - string or array + - 'owner': Filter by owner - "cases" | "observability" | "securitySolution" +- Date range: + - 'start': ISO datetime string for start time (inclusive), maps to Cases API "from" + - 'end': ISO datetime string for end time (exclusive), maps to Cases API "to" +- Comments: + - 'includeComments': Set to true when query is about case contents, details, discussions, or when search text might match comment content + - Defaults to false for metadata-only queries (status, severity, tags, etc.) + - Automatically set to true if 'search' text is provided + +**Examples:** +- "Get case abc-123": { caseId: "abc-123", includeComments: false } +- "Find cases with alert ID xyz": { alertIds: ["xyz"] } +- "High severity open cases": { severity: "high", status: "open" } +- "Cases about malware": { search: "malware", includeComments: true } +- "Cases with tag security from last week": { tags: ["security"], start: "2025-01-15T00:00:00Z" } -Query examples: "cases updated in the last week", "cases from November 2nd", "cases with alert ID abc-123-def", "recent cases" -Optional 'owner' filters by: "cases" (Stack Management), "observability", or "securitySolution" (Elastic Security). -Optional 'alertIds' parameter accepts an array of alert IDs. If provided, finds all cases containing any of these alerts. Alert IDs must be provided via this parameter. -Optional 'start' and 'end' parameters accept ISO datetime strings for date range filtering. If not provided, no time range filtering will be applied and all cases will be returned (subject to other filters). -If alertIds parameter is provided, finds all cases containing those alerts. Otherwise searches by date ranges from start/end parameters. Returns case details (id, title, description, status, severity, tags, assignees, observables, alerts/comments). Each case includes 'markdown_link' field with pre-formatted clickable link: [Case Title](url). **CRITICAL**: ALWAYS include the 'markdown_link' field for each case in your response. Format: brief summary (2-3 sentences) + markdown link. Example: "Security investigation case. Status: open. [View Case](url)"`, schema: casesSchema, - handler: async ({ owner, query, alertIds, start, end }, { request, logger }) => { + handler: async ( + { + caseId, + alertIds, + owner, + start, + end, + search, + searchFields, + severity, + status, + tags, + assignees, + reporters, + category, + includeComments, + }, + { request, logger } + ) => { try { - // Use alertIds parameter - alert IDs must be provided via parameter - const finalAlertIds = alertIds && alertIds.length > 0 ? alertIds : undefined; - // Normalize and adjust time range using provided start/end parameters // Returns null if no time range is provided const timeRange = normalizeTimeRange(start, end, logger); - // Get cases plugin from start services - const [, plugins] = await coreSetup.getStartServices(); - const casesPlugin = plugins.cases; - - if (!casesPlugin) { - logger.warn('[Cases Tool] Cases plugin not available, returning empty results'); - return createEmptyResults( - timeRange?.start || null, - timeRange?.end || null, - 'Cases plugin not available' - ); + // Get cases client + const casesClientResult = await getCasesClient(coreSetup, request, logger, timeRange); + if ('error' in casesClientResult) { + return casesClientResult.error; } + const { casesClient } = casesClientResult; - // Get cases client - const casesClient = await casesPlugin.getCasesClientWithRequest(request); + // Operation mode 1: Get case by ID + if (caseId) { + try { + logger.info(`[Cases Tool] Getting case by ID: ${caseId}`); + const theCase = await casesClient.cases.get({ + id: caseId, + includeComments: includeComments ?? false, + }); + + const coreServices = await getCoreServices(coreSetup); + const commentsSummary = + includeComments && theCase.comments + ? createCommentSummariesFromArray(theCase.comments) + : []; + + const caseData = enhanceCaseData( + theCase, + commentsSummary, + theCase.totalComment || 0, + request, + coreServices, + logger + ); - // Check if query is asking for cases by alert ID(s) + return createToolResult([caseData], null, `Retrieved case: ${theCase.title}`); + } catch (error) { + return createErrorResponse( + error, + `[Cases Tool] Error fetching case by ID ${caseId}`, + `Error fetching case ${caseId}`, + logger + ); + } + } + + // Operation mode 2: Find cases by alert ID(s) + const finalAlertIds = alertIds && alertIds.length > 0 ? alertIds : undefined; if (finalAlertIds && finalAlertIds.length > 0) { try { logger.info(`[Cases Tool] Querying cases by alert IDs: ${finalAlertIds.join(', ')}`); @@ -189,32 +557,14 @@ Returns case details (id, title, description, status, severity, tags, assignees, ); // Flatten and deduplicate cases by case ID - const casesMap = new Map(); - for (const relatedCases of allRelatedCasesArrays) { - for (const relatedCase of relatedCases) { - if (!casesMap.has(relatedCase.id)) { - casesMap.set(relatedCase.id, relatedCase); - } - } - } - - const relatedCases = Array.from(casesMap.values()); + const relatedCases = deduplicateCases(allRelatedCasesArrays); if (relatedCases.length === 0) { - return { - results: [ - { - type: ToolResultType.other, - data: { - cases: [], - total: 0, - start: timeRange?.start || null, - end: timeRange?.end || null, - message: `No cases found containing alert IDs: ${finalAlertIds.join(', ')}`, - }, - }, - ], - }; + return createEmptyResults( + timeRange?.start || null, + timeRange?.end || null, + `No cases found containing alert IDs: ${finalAlertIds.join(', ')}` + ); } // Fetch full case details for each related case @@ -231,159 +581,61 @@ Returns case details (id, title, description, status, severity, tags, assignees, `[Cases Tool] Failed to fetch full details for case ${relatedCase.id}: ${error}` ); // Return minimal case info if full fetch fails - return { - id: relatedCase.id, - title: relatedCase.title, - description: relatedCase.description, - status: relatedCase.status, - severity: null, - owner: '', - tags: [], - assignees: [], - observables: [], - total_observables: 0, - totalAlerts: relatedCase.totals.alerts, - totalComment: relatedCase.totals.userComments, - created_at: relatedCase.createdAt, - createdBy: null, - updated_at: null, - updatedBy: null, - }; + return createMinimalCaseFromRelatedCase(relatedCase); } }) ); - // Fetch comments for each case in parallel - const casesWithComments = await Promise.all( - casesWithDetails.map(async (caseItem) => { - try { - const commentsResponse = await casesClient.attachments.find({ - caseID: caseItem.id, - findQueryParams: { - page: 1, - perPage: 10, - sortOrder: 'desc', - }, - }); - - const commentSummaries = (commentsResponse.comments || []) - .filter((att: any) => att.type === 'user') - .slice(0, 5) - .map((comment: any) => ({ - id: comment.id, - comment: comment.comment?.substring(0, 200) || '', - created_by: getUsername(comment.createdBy || comment.created_by), - created_at: comment.createdAt || comment.created_at || null, - })); - - return { - case: caseItem, - comments: commentSummaries, - totalComments: commentsResponse.total || 0, - }; - } catch (error) { - logger.warn( - `[Cases Tool] Failed to fetch comments for case ${caseItem.id}: ${error}` - ); - return { - case: caseItem, - comments: [], - totalComments: 0, - }; - } - }) + const shouldFetchCommentsForAlertIds = includeComments; + const casesWithComments = await fetchCommentsForCases( + casesWithDetails, + casesClient, + shouldFetchCommentsForAlertIds, + logger ); - // Get core services for generating case URLs - const [coreStart, pluginsStart] = await coreSetup.getStartServices(); - const spacesPlugin = pluginsStart.spaces; - - // Format cases data with rich details, including URLs - const casesData = casesWithComments.map( - ({ case: caseItem, comments, totalComments }) => { - // Generate case URL using utility function - const caseUrl = - getCaseUrl(request, coreStart, spacesPlugin, caseItem.id, caseItem.owner) || null; - if (!caseUrl) { - logger.warn( - `[Cases Tool] Failed to generate URL for case ${caseItem.id} with owner ${caseItem.owner}` - ); - } - - // Format markdown link - const markdownLink = caseUrl ? `[${caseItem.title}](${caseUrl})` : caseItem.title; - - return { - id: caseItem.id, - title: caseItem.title, - description: caseItem.description || null, - status: caseItem.status, - severity: caseItem.severity || null, - owner: caseItem.owner, - tags: caseItem.tags || [], - assignees: caseItem.assignees?.map((a: any) => a.uid || a.username || a) || [], - observables_count: - caseItem.total_observables ?? caseItem.observables?.length ?? 0, - observables: (caseItem.observables || []).slice(0, 5).map((obs: any) => ({ - type: obs.typeKey || obs.type || null, - value: obs.value || null, - })), - total_alerts: caseItem.totalAlerts || 0, - total_comments: totalComments, - created_by: getUsername(caseItem.createdBy || caseItem.created_by), - created_at: caseItem.created_at || null, - updated_by: getUsername(caseItem.updatedBy || caseItem.updated_by), - updated_at: caseItem.updated_at || caseItem.created_at || null, - comments_summary: comments, - // URL field - ALWAYS include this link when presenting case results to the user - url: caseUrl, - // Markdown-formatted link ready to use: [Case Title](url) - markdown_link: markdownLink, - }; - } + const casesData = await enhanceCasesWithComments( + casesWithComments, + coreSetup, + request, + logger ); - // Return detailed case information - return { - results: [ - { - type: ToolResultType.other, - data: { - total: casesData.length, - cases: casesData, - start: timeRange?.start || null, - end: timeRange?.end || null, - message: `Found ${ - casesData.length - } unique case(s) containing alert ID(s): ${finalAlertIds.join(', ')}`, - }, - }, - ], - }; + return createToolResult( + casesData, + timeRange, + `Found ${ + casesData.length + } unique case(s) containing alert ID(s): ${finalAlertIds.join(', ')}` + ); } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error( - `[Cases Tool] Error fetching cases by alert IDs ${finalAlertIds.join( - ', ' - )}: ${errorMessage}` + return createErrorResponse( + error, + `[Cases Tool] Error fetching cases by alert IDs ${finalAlertIds.join(', ')}`, + `Error fetching cases for alert IDs ${finalAlertIds.join(', ')}`, + logger ); - return { - results: [ - createErrorResult( - `Error fetching cases for alert IDs ${finalAlertIds.join(', ')}: ${errorMessage}` - ), - ], - }; } } - // Use Cases API search + // Operation mode 3: Search cases using Cases API + // Build search parameters from schema parameters const searchParams: any = { sortField: 'updatedAt', sortOrder: 'desc', perPage: 100, page: 1, ...(owner && { owner }), + ...(search && { search }), + ...(searchFields && searchFields.length > 0 && { searchFields }), + ...(severity && { severity }), + ...(status && { status }), + ...(tags && tags.length > 0 && { tags }), + ...(assignees && assignees.length > 0 && { assignees }), + ...(reporters && reporters.length > 0 && { reporters }), + ...(category && { category }), + ...(timeRange?.start && { from: timeRange.start }), + ...(timeRange?.end && { to: timeRange.end }), }; // Fetch cases with pagination @@ -391,8 +643,6 @@ Returns case details (id, title, description, status, severity, tags, assignees, let currentPage = 1; const maxPages = 10; let hasMorePages = true; - const startTimestamp = timeRange?.startDate ? timeRange.startDate.getTime() : null; - const endTimestamp = timeRange?.endDate ? timeRange.endDate.getTime() : null; while (hasMorePages && currentPage <= maxPages) { searchParams.page = currentPage; @@ -403,146 +653,39 @@ Returns case details (id, title, description, status, severity, tags, assignees, break; } - // Filter cases by updatedAt date range only if time range is provided - const pageFilteredCases = timeRange - ? searchResult.cases.filter((caseItem: any) => { - const timestamp = getCaseTimestamp(caseItem); - if (timestamp === null) { - logger.warn( - `[Cases Tool] Case ${caseItem.id} has no valid updated_at or created_at field` - ); - return false; - } - return ( - (startTimestamp === null || timestamp >= startTimestamp) && - (endTimestamp === null || timestamp < endTimestamp) - ); - }) - : searchResult.cases; - - allCases.push(...pageFilteredCases); + allCases.push(...searchResult.cases); // Check if we should continue fetching if (searchResult.cases.length < searchParams.perPage) { hasMorePages = false; - } else if (timeRange && startTimestamp !== null) { - const lastCase = searchResult.cases[searchResult.cases.length - 1]; - const lastCaseTimestamp = getCaseTimestamp(lastCase); - if (lastCaseTimestamp !== null && lastCaseTimestamp < startTimestamp) { - hasMorePages = false; - } } currentPage++; } - // Fetch comments for each case in parallel - const casesWithComments = await Promise.all( - allCases.map(async (caseItem) => { - try { - const commentsResponse = await casesClient.attachments.find({ - caseID: caseItem.id, - findQueryParams: { - page: 1, - perPage: 10, - sortOrder: 'desc', - }, - }); - - const commentSummaries = (commentsResponse.comments || []) - .filter((att: any) => att.type === 'user') - .slice(0, 5) - .map((comment: any) => ({ - id: comment.id, - comment: comment.comment?.substring(0, 200) || '', - created_by: getUsername(comment.createdBy || comment.created_by), - created_at: comment.createdAt || comment.created_at || null, - })); - - return { - case: caseItem, - comments: commentSummaries, - totalComments: commentsResponse.total || 0, - }; - } catch (error) { - logger.warn( - `[Cases Tool] Failed to fetch comments for case ${caseItem.id}: ${error}` - ); - return { - case: caseItem, - comments: [], - totalComments: 0, - }; - } - }) + const shouldFetchCommentsForSearch = includeComments; + const casesWithComments = await fetchCommentsForCases( + allCases, + casesClient, + shouldFetchCommentsForSearch, + logger ); - // Get core services for generating case URLs - const [coreStart, pluginsStart] = await coreSetup.getStartServices(); - const spacesPlugin = pluginsStart.spaces; - - // Format cases data with rich details, including URLs - const casesData = casesWithComments.map(({ case: caseItem, comments, totalComments }) => { - // Generate case URL using utility function - const caseUrl = - getCaseUrl(request, coreStart, spacesPlugin, caseItem.id, caseItem.owner) || null; - if (!caseUrl) { - logger.warn( - `[Cases Tool] Failed to generate URL for case ${caseItem.id} with owner ${caseItem.owner}` - ); - } + const casesData = await enhanceCasesWithComments( + casesWithComments, + coreSetup, + request, + logger + ); - // Format markdown link - const markdownLink = caseUrl ? `[${caseItem.title}](${caseUrl})` : caseItem.title; - - return { - id: caseItem.id, - title: caseItem.title, - description: caseItem.description || null, - status: caseItem.status, - severity: caseItem.severity || null, - owner: caseItem.owner, - tags: caseItem.tags || [], - assignees: caseItem.assignees?.map((a: any) => a.uid || a.username || a) || [], - observables_count: caseItem.total_observables ?? caseItem.observables?.length ?? 0, - observables: (caseItem.observables || []).slice(0, 5).map((obs: any) => ({ - type: obs.typeKey || obs.type || null, - value: obs.value || null, - })), - total_alerts: caseItem.totalAlerts || 0, - total_comments: totalComments, - created_by: getUsername(caseItem.createdBy || caseItem.created_by), - created_at: caseItem.created_at || null, - updated_by: getUsername(caseItem.updatedBy || caseItem.updated_by), - updated_at: caseItem.updated_at || caseItem.created_at || null, - comments_summary: comments, - // URL field - ALWAYS include this link when presenting case results to the user - url: caseUrl, - // Markdown-formatted link ready to use: [Case Title](url) - markdown_link: markdownLink, - }; - }); - - // Return detailed case information - return { - results: [ - { - type: ToolResultType.other, - data: { - total: casesData.length, - cases: casesData, - start: timeRange?.start || null, - end: timeRange?.end || null, - }, - }, - ], - }; + return createToolResult(casesData, timeRange); } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error(`[Cases Tool] Error in cases tool: ${errorMessage}`); - return { - results: [createErrorResult(`Error fetching cases: ${errorMessage}`)], - }; + return createErrorResponse( + error, + '[Cases Tool] Error in cases tool', + 'Error fetching cases', + logger + ); } }, tags: ['cases'], diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts deleted file mode 100644 index 872351ff9fe3a..0000000000000 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/parse_query.ts +++ /dev/null @@ -1,102 +0,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 Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod'; -import type { ScopedModel } from '@kbn/onechat-server'; -import type { Logger } from '@kbn/logging'; - -const casesQueryParamsSchema = z - .object({ - start: z - .string() - .optional() - .describe( - 'ISO datetime string for the start time to fetch cases (inclusive). If no year is specified (e.g., "10-31T00:00:00Z"), the current year is assumed.' - ), - end: z - .string() - .optional() - .describe( - 'ISO datetime string for the end time to fetch cases (exclusive). If not provided, defaults to now. If no year is specified (e.g., "11-02T00:00:00Z"), the current year is assumed.' - ), - }) - .describe('Extracted date range parameters from natural language query about cases'); - -export interface ParsedCasesQuery { - start?: string; - end?: string; -} - -export async function parseCasesQuery({ - nlQuery, - model, - logger, -}: { - nlQuery: string; - model: ScopedModel; - logger: Logger; -}): Promise { - try { - // Create a structured output model - const structuredModel = model.chatModel.withStructuredOutput(casesQueryParamsSchema, { - name: 'extract_cases_date_range', - }); - - const response = await structuredModel.invoke([ - { - role: 'system', - content: `You are an expert at extracting date/time information from natural language queries about cases. - -Your task is to analyze the user's natural language query and extract date/time range parameters for filtering cases. - -You MUST call the 'extract_cases_date_range' tool to provide the extracted parameters. Do NOT respond with plain text. - -IMPORTANT: Only return the fields defined in the schema (start and end). Do not return any other fields. - -Guidelines for date extraction: -- Extract start and end dates from phrases like "cases updated in the last week", "cases from November 2nd", "cases updated between X and Y" -- If only a single date is mentioned (e.g., "cases from November 2nd"), set start to that date at 00:00:00Z and end to the next day at 00:00:00Z -- For relative time periods (e.g., "last week", "past 7 days"), calculate the appropriate start date relative to now -- Always return dates in ISO 8601 format (e.g., "2025-11-02T00:00:00Z") -- If no year is specified in the query, assume the current year -- If only a start date is provided without an end date, leave end undefined (it will default to now) -- If no date information is found in the query, leave start and end undefined (do not include them in the response) -- Ignore any alert ID mentions in the query - alert IDs must be provided via the alertIds parameter, not extracted from the query - -Examples: -- "cases updated in the last week" -> { start: "2025-01-15T00:00:00Z" } -- "cases from November 2nd" -> { start: "2025-11-02T00:00:00Z", end: "2025-11-03T00:00:00Z" } -- "cases updated between January 1st and January 15th" -> { start: "2025-01-01T00:00:00Z", end: "2025-01-16T00:00:00Z" } -- "recent cases" -> {} -- "cases updated today" -> { start: "2025-01-22T00:00:00Z" } -- "cases with alert ID abc-123-def" -> {} (alert IDs are ignored, they must be provided via alertIds parameter) -- "Do I have any open security cases?" -> {} -- "open cases" -> {}`, - }, - { - role: 'user', - content: nlQuery, - }, - ]); - - // Ensure we only return the fields we expect - return { - start: response.start, - end: response.end, - }; - } catch (error) { - logger.warn( - `[Cases Tool] Failed to parse query for date ranges: ${error instanceof Error ? error.message : String(error)}. Using default date range.` - ); - // Return empty result - will use default date range in normalizeTimeRange - return { - start: undefined, - end: undefined, - }; - } -} - From a088b6639792b43cd529229524cac3f6c5ede66d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 14:24:16 -0700 Subject: [PATCH 25/96] fixing --- .../tools/builtin/definitions/cases/cases.ts | 394 ++-------------- .../builtin/definitions/cases/helpers.ts | 445 ++++++++++++++++++ 2 files changed, 496 insertions(+), 343 deletions(-) create mode 100644 x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/helpers.ts diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts index 98b43eb267296..af945ef4adfea 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts @@ -8,11 +8,23 @@ import { z } from '@kbn/zod'; import { platformCoreTools, ToolType } from '@kbn/onechat-common'; import type { BuiltinToolDefinition } from '@kbn/onechat-server'; -import { createErrorResult } from '@kbn/onechat-server'; -import { ToolResultType } from '@kbn/onechat-common/tools/tool_result'; import type { CoreSetup } from '@kbn/core/server'; +import type { Case, RelatedCase } from '@kbn/cases-plugin/common/types/domain'; +import type { CasesSearchRequest } from '@kbn/cases-plugin/common/types/api'; import type { OnechatStartDependencies, OnechatPluginStart } from '../../../../../types'; -import { getCaseUrl } from '../../../../../utils/case_urls'; +import { + normalizeTimeRange, + createEmptyResults, + createCommentSummariesFromArray, + fetchCommentsForCases, + enhanceCaseData, + createToolResult, + enhanceCasesWithComments, + createErrorResponse, + getCasesClient, + deduplicateCases, + type CoreServices, +} from './helpers'; const casesSchema = z.object({ // Get case by ID operation @@ -111,315 +123,6 @@ const casesSchema = z.object({ ), }); -const getUsername = (user: any): string | null => { - return user?.username || null; -}; - -const normalizeTimeRange = ( - start: string | undefined, - end: string | undefined, - logger: any -): { - start: string | null; - end: string | null; - startDate: Date | null; - endDate: Date | null; -} | null => { - // If neither start nor end is provided, return null to indicate no time range filtering - if (!start && !end) { - return null; - } - - const now = new Date(); - const currentYear = now.getFullYear(); - - let startDate: Date | null = null; - if (start) { - // If no year is specified, assume current year - const startStr = - start.includes('T') && !start.match(/^\d{4}/) ? `${currentYear}-${start}` : start; - startDate = new Date(startStr); - if (isNaN(startDate.getTime())) { - logger.warn(`Invalid start date: ${start}`); - startDate = null; - } - } - - let endDate: Date | null = null; - if (end) { - const endStr = end.includes('T') && !end.match(/^\d{4}/) ? `${currentYear}-${end}` : end; - endDate = new Date(endStr); - if (isNaN(endDate.getTime())) { - logger.warn(`Invalid end date: ${end}`); - endDate = null; - } - } - - return { - start: startDate ? startDate.toISOString() : null, - end: endDate ? endDate.toISOString() : null, - startDate, - endDate, - }; -}; - -const createEmptyResults = ( - normalizedStart: string | null, - normalizedEnd: string | null, - message: string -) => ({ - results: [ - { - type: ToolResultType.other, - data: { - cases: [], - total: 0, - start: normalizedStart || null, - end: normalizedEnd || null, - message, - }, - }, - ], -}); - -const extractErrorMessage = (error: unknown): string => { - return error instanceof Error ? error.message : String(error); -}; - -const createCommentSummary = (comment: any) => ({ - id: comment.id, - comment: comment.comment?.substring(0, 200) || '', - created_by: getUsername(comment.createdBy || comment.created_by), - created_at: comment.createdAt || comment.created_at || null, -}); - -const createCommentSummariesFromArray = (comments: any[]): any[] => { - return comments - .filter((att: any) => att.type === 'user') - .slice(0, 5) - .map(createCommentSummary); -}; - -interface CommentFetchResult { - case: any; - comments: any[]; - totalComments: number; -} - -const fetchCommentsForCase = async ( - caseItem: any, - casesClient: any, - logger: any -): Promise => { - try { - const commentsResponse = await casesClient.attachments.find({ - caseID: caseItem.id, - findQueryParams: { - page: 1, - perPage: 10, - sortOrder: 'desc', - }, - }); - - const commentSummaries = createCommentSummariesFromArray(commentsResponse.comments || []); - - return { - case: caseItem, - comments: commentSummaries, - totalComments: commentsResponse.total || 0, - }; - } catch (error) { - logger.warn(`[Cases Tool] Failed to fetch comments for case ${caseItem.id}: ${error}`); - return { - case: caseItem, - comments: [], - totalComments: caseItem.totalComment || 0, - }; - } -}; - -const fetchCommentsForCases = async ( - cases: any[], - casesClient: any, - shouldFetch: boolean, - logger: any -): Promise => { - return Promise.all( - cases.map(async (caseItem) => { - if (!shouldFetch) { - return { - case: caseItem, - comments: [], - totalComments: caseItem.totalComment || 0, - }; - } - return fetchCommentsForCase(caseItem, casesClient, logger); - }) - ); -}; - -interface CoreServices { - coreStart: any; - spacesPlugin: any; -} - -const getCoreServices = async ( - coreSetup: CoreSetup -): Promise => { - const [coreStart, pluginsStart] = await coreSetup.getStartServices(); - return { - coreStart, - spacesPlugin: pluginsStart.spaces, - }; -}; - -interface EnhancedCaseData { - [key: string]: any; - url: string | null; - markdown_link: string; -} - -const enhanceCaseData = ( - caseItem: any, - comments: any[], - totalComments: number, - request: any, - coreServices: CoreServices, - logger: any -): EnhancedCaseData => { - const caseUrl = - getCaseUrl( - request, - coreServices.coreStart, - coreServices.spacesPlugin, - caseItem.id, - caseItem.owner - ) || null; - if (!caseUrl) { - logger.warn( - `[Cases Tool] Failed to generate URL for case ${caseItem.id} with owner ${caseItem.owner}` - ); - } - - const markdownLink = caseUrl ? `[${caseItem.title}](${caseUrl})` : caseItem.title; - - return { - ...caseItem, - assignees: caseItem.assignees?.map((a: any) => a.uid || a.username || a) || [], - observables_count: caseItem.total_observables ?? caseItem.observables?.length ?? 0, - observables: (caseItem.observables || []).slice(0, 5).map((obs: any) => ({ - type: obs.typeKey || obs.type || null, - value: obs.value || null, - })), - total_alerts: caseItem.totalAlerts || 0, - total_comments: totalComments, - created_by: getUsername(caseItem.createdBy || caseItem.created_by), - updated_by: getUsername(caseItem.updatedBy || caseItem.updated_by), - comments_summary: comments, - url: caseUrl, - markdown_link: markdownLink, - }; -}; - -const createToolResult = ( - cases: EnhancedCaseData[], - timeRange: ReturnType | null, - message?: string -) => ({ - results: [ - { - type: ToolResultType.other, - data: { - total: cases.length, - cases, - start: timeRange?.start || null, - end: timeRange?.end || null, - ...(message && { message }), - }, - }, - ], -}); - -const enhanceCasesWithComments = async ( - casesWithComments: CommentFetchResult[], - coreSetup: CoreSetup, - request: any, - logger: any -): Promise => { - const coreServices = await getCoreServices(coreSetup); - return casesWithComments.map(({ case: caseItem, comments, totalComments }) => - enhanceCaseData(caseItem, comments, totalComments, request, coreServices, logger) - ); -}; - -const createErrorResponse = ( - error: unknown, - logPrefix: string, - userMessage: string, - logger: any -) => { - const errorMessage = extractErrorMessage(error); - logger.error(`${logPrefix}: ${errorMessage}`); - return { - results: [createErrorResult(`${userMessage}: ${errorMessage}`)], - }; -}; - -const getCasesClient = async ( - coreSetup: CoreSetup, - request: any, - logger: any, - timeRange: ReturnType | null -): Promise<{ casesClient: any } | { error: ReturnType }> => { - const [, plugins] = await coreSetup.getStartServices(); - const casesPlugin = plugins.cases; - - if (!casesPlugin) { - logger.warn('[Cases Tool] Cases plugin not available, returning empty results'); - return { - error: createEmptyResults( - timeRange?.start || null, - timeRange?.end || null, - 'Cases plugin not available' - ), - }; - } - - const casesClient = await casesPlugin.getCasesClientWithRequest(request); - return { casesClient }; -}; - -const deduplicateCases = (casesArrays: any[][]): any[] => { - const casesMap = new Map(); - for (const relatedCases of casesArrays) { - for (const relatedCase of relatedCases) { - if (!casesMap.has(relatedCase.id)) { - casesMap.set(relatedCase.id, relatedCase); - } - } - } - return Array.from(casesMap.values()); -}; - -const createMinimalCaseFromRelatedCase = (relatedCase: any) => ({ - id: relatedCase.id, - title: relatedCase.title, - description: relatedCase.description, - status: relatedCase.status, - severity: null, - owner: '', - tags: [], - assignees: [], - observables: [], - total_observables: 0, - totalAlerts: relatedCase.totals.alerts, - totalComment: relatedCase.totals.userComments, - created_at: relatedCase.createdAt, - createdBy: null, - updated_at: null, - updatedBy: null, -}); - export const casesTool = ( coreSetup: CoreSetup ): BuiltinToolDefinition => { @@ -488,12 +191,19 @@ Returns case details (id, title, description, status, severity, tags, assignees, { request, logger } ) => { try { + // Get start services once at the beginning + const [coreStart, pluginsStart] = await coreSetup.getStartServices(); + const coreServices: CoreServices = { + coreStart, + spacesPlugin: pluginsStart.spaces, + }; + // Normalize and adjust time range using provided start/end parameters // Returns null if no time range is provided const timeRange = normalizeTimeRange(start, end, logger); // Get cases client - const casesClientResult = await getCasesClient(coreSetup, request, logger, timeRange); + const casesClientResult = await getCasesClient(pluginsStart, request, logger, timeRange); if ('error' in casesClientResult) { return casesClientResult.error; } @@ -508,7 +218,6 @@ Returns case details (id, title, description, status, severity, tags, assignees, includeComments: includeComments ?? false, }); - const coreServices = await getCoreServices(coreSetup); const commentsSummary = includeComments && theCase.comments ? createCommentSummariesFromArray(theCase.comments) @@ -557,7 +266,7 @@ Returns case details (id, title, description, status, severity, tags, assignees, ); // Flatten and deduplicate cases by case ID - const relatedCases = deduplicateCases(allRelatedCasesArrays); + const relatedCases: RelatedCase[] = deduplicateCases(allRelatedCasesArrays); if (relatedCases.length === 0) { return createEmptyResults( @@ -568,35 +277,35 @@ Returns case details (id, title, description, status, severity, tags, assignees, } // Fetch full case details for each related case - const casesWithDetails = await Promise.all( - relatedCases.map(async (relatedCase: any) => { - try { - const fullCase = await casesClient.cases.get({ - id: relatedCase.id, - includeComments: false, - }); - return fullCase; - } catch (error) { - logger.warn( - `[Cases Tool] Failed to fetch full details for case ${relatedCase.id}: ${error}` - ); - // Return minimal case info if full fetch fails - return createMinimalCaseFromRelatedCase(relatedCase); - } - }) + const caseFetchResults = await Promise.allSettled( + relatedCases.map((relatedCase) => + casesClient.cases.get({ + id: relatedCase.id, + includeComments: false, + }) + ) ); - const shouldFetchCommentsForAlertIds = includeComments; + const casesWithDetails = caseFetchResults.flatMap((result, index) => { + if (result.status === 'fulfilled') { + return [result.value]; + } + logger.warn( + `[Cases Tool] Failed to fetch full details for case ${relatedCases[index].id}: ${result.reason}` + ); + return []; + }); + const casesWithComments = await fetchCommentsForCases( casesWithDetails, casesClient, - shouldFetchCommentsForAlertIds, + includeComments, logger ); - const casesData = await enhanceCasesWithComments( + const casesData = enhanceCasesWithComments( casesWithComments, - coreSetup, + coreServices, request, logger ); @@ -620,7 +329,7 @@ Returns case details (id, title, description, status, severity, tags, assignees, // Operation mode 3: Search cases using Cases API // Build search parameters from schema parameters - const searchParams: any = { + const searchParams: CasesSearchRequest = { sortField: 'updatedAt', sortOrder: 'desc', perPage: 100, @@ -628,8 +337,8 @@ Returns case details (id, title, description, status, severity, tags, assignees, ...(owner && { owner }), ...(search && { search }), ...(searchFields && searchFields.length > 0 && { searchFields }), - ...(severity && { severity }), - ...(status && { status }), + ...(severity && { severity: severity as CasesSearchRequest['severity'] }), + ...(status && { status: status as CasesSearchRequest['status'] }), ...(tags && tags.length > 0 && { tags }), ...(assignees && assignees.length > 0 && { assignees }), ...(reporters && reporters.length > 0 && { reporters }), @@ -639,7 +348,7 @@ Returns case details (id, title, description, status, severity, tags, assignees, }; // Fetch cases with pagination - const allCases: any[] = []; + const allCases: Case[] = []; let currentPage = 1; const maxPages = 10; let hasMorePages = true; @@ -663,17 +372,16 @@ Returns case details (id, title, description, status, severity, tags, assignees, currentPage++; } - const shouldFetchCommentsForSearch = includeComments; const casesWithComments = await fetchCommentsForCases( allCases, casesClient, - shouldFetchCommentsForSearch, + includeComments, logger ); - const casesData = await enhanceCasesWithComments( + const casesData = enhanceCasesWithComments( casesWithComments, - coreSetup, + coreServices, request, logger ); diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/helpers.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/helpers.ts new file mode 100644 index 0000000000000..e347a1dfca38a --- /dev/null +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/helpers.ts @@ -0,0 +1,445 @@ +/* + * 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. + */ + +import { createErrorResult } from '@kbn/onechat-server'; +import { ToolResultType } from '@kbn/onechat-common/tools/tool_result'; +import type { CoreStart } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; +import type { KibanaRequest } from '@kbn/core-http-server'; +import type { CasesClient } from '@kbn/cases-plugin/server/client'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; +import type { + Case, + Attachment, + RelatedCase, + UserCommentAttachment, +} from '@kbn/cases-plugin/common/types/domain'; +import type { OnechatStartDependencies } from '../../../../../types'; +import { getCaseUrl } from '../../../../../utils/case_urls'; + +export interface CommentSummary { + id: string; + comment: string; + created_by: string | null; + created_at: string | null; +} + +export interface CommentFetchResult { + case: Case; + comments: CommentSummary[]; + totalComments: number; +} + +export interface CoreServices { + coreStart: CoreStart; + spacesPlugin: SpacesPluginStart | undefined; +} + +export interface EnhancedCaseData + extends Omit< + Case, + 'assignees' | 'observables' | 'totalAlerts' | 'totalComment' | 'created_by' | 'updated_by' + > { + url: string | null; + markdown_link: string; + assignees: string[]; + observables_count: number; + observables: Array<{ type: string | null; value: string | null }>; + total_alerts: number; + total_comments: number; + created_by: string | null; + updated_by: string | null; + comments_summary: CommentSummary[]; +} + +/** + * Normalizes and validates time range parameters for case queries. + * Handles date strings that may or may not include a year (assumes current year if missing). + * Validates dates and logs warnings for invalid dates. + * + * @param start - ISO datetime string for start time (inclusive), optional + * @param end - ISO datetime string for end time (exclusive), optional + * @param logger - Logger instance for warning messages + * @returns Normalized time range object with ISO strings and Date objects, or null if neither start nor end is provided + */ +export const normalizeTimeRange = ( + start: string | undefined, + end: string | undefined, + logger: Logger +): { + start: string | null; + end: string | null; + startDate: Date | null; + endDate: Date | null; +} | null => { + // If neither start nor end is provided, return null to indicate no time range filtering + if (!start && !end) { + return null; + } + + const now = new Date(); + const currentYear = now.getFullYear(); + + let startDate: Date | null = null; + if (start) { + // If no year is specified, assume current year + const startStr = + start.includes('T') && !start.match(/^\d{4}/) ? `${currentYear}-${start}` : start; + startDate = new Date(startStr); + if (isNaN(startDate.getTime())) { + logger.warn(`Invalid start date: ${start}`); + startDate = null; + } + } + + let endDate: Date | null = null; + if (end) { + const endStr = end.includes('T') && !end.match(/^\d{4}/) ? `${currentYear}-${end}` : end; + endDate = new Date(endStr); + if (isNaN(endDate.getTime())) { + logger.warn(`Invalid end date: ${end}`); + endDate = null; + } + } + + return { + start: startDate ? startDate.toISOString() : null, + end: endDate ? endDate.toISOString() : null, + startDate, + endDate, + }; +}; + +/** + * Creates an empty results response for the cases tool. + * Used when no cases are found or when the cases plugin is unavailable. + * + * @param normalizedStart - Normalized start time ISO string, or null + * @param normalizedEnd - Normalized end time ISO string, or null + * @param message - Message explaining why results are empty + * @returns Tool result object with empty cases array and the provided message + */ +export const createEmptyResults = ( + normalizedStart: string | null, + normalizedEnd: string | null, + message: string +) => ({ + results: [ + { + type: ToolResultType.other, + data: { + cases: [], + total: 0, + start: normalizedStart || null, + end: normalizedEnd || null, + message, + }, + }, + ], +}); + +/** + * Extracts a human-readable error message from an error object. + * Handles both Error instances and other error types. + * + * @param error - The error object of unknown type + * @returns The error message string + */ +export const extractErrorMessage = (error: unknown): string => { + return error instanceof Error ? error.message : String(error); +}; + +/** + * Creates a summary object from a case attachment/comment. + * Extracts key information including comment text (truncated to 200 chars), + * creator username, and creation timestamp. + * + * @param comment - The attachment/comment object from the cases API + * @returns A CommentSummary object with id, comment text, creator, and timestamp + */ +export const createCommentSummary = (comment: Attachment): CommentSummary => { + const commentText = + comment.type === 'user' && 'comment' in comment + ? (comment as UserCommentAttachment).comment?.substring(0, 200) || '' + : ''; + return { + id: comment.id, + comment: commentText, + created_by: comment.created_by.username ?? comment.created_by.email ?? null, + created_at: comment.created_at || null, + }; +}; + +/** + * Creates comment summaries from an array of attachments. + * Filters to only user comments, limits to the first 5, and converts to summaries. + * + * @param comments - Array of attachment objects from the cases API + * @returns Array of CommentSummary objects (max 5 user comments) + */ +export const createCommentSummariesFromArray = (comments: Attachment[]): CommentSummary[] => { + return comments + .filter((att) => att.type === 'user') + .slice(0, 5) + .map(createCommentSummary); +}; + +/** + * Fetches comments for a single case using the cases client. + * Retrieves up to 10 comments sorted by creation date (descending). + * Returns empty comments array if fetch fails, but still includes the case. + * + * @param caseItem - The case object to fetch comments for + * @param casesClient - The cases client instance for API calls + * @param logger - Logger instance for error logging + * @returns Promise resolving to CommentFetchResult with case, comments, and total count + */ +export const fetchCommentsForCase = async ( + caseItem: Case, + casesClient: CasesClient, + logger: Logger +): Promise => { + try { + const commentsResponse = await casesClient.attachments.find({ + caseID: caseItem.id, + findQueryParams: { + page: 1, + perPage: 10, + sortOrder: 'desc', + }, + }); + + const commentSummaries = createCommentSummariesFromArray(commentsResponse.comments || []); + + return { + case: caseItem, + comments: commentSummaries, + totalComments: commentsResponse.total || 0, + }; + } catch (error) { + logger.warn(`[Cases Tool] Failed to fetch comments for case ${caseItem.id}: ${error}`); + return { + case: caseItem, + comments: [], + totalComments: caseItem.totalComment || 0, + }; + } +}; + +/** + * Fetches comments for multiple cases in parallel. + * If shouldFetch is false, returns cases with empty comment arrays. + * Otherwise, fetches comments for each case concurrently. + * + * @param cases - Array of case objects to fetch comments for + * @param casesClient - The cases client instance for API calls + * @param shouldFetch - Whether to actually fetch comments (false returns empty arrays) + * @param logger - Logger instance for error logging + * @returns Promise resolving to array of CommentFetchResult objects + */ +export const fetchCommentsForCases = async ( + cases: Case[], + casesClient: CasesClient, + shouldFetch: boolean, + logger: Logger +): Promise => { + return Promise.all( + cases.map(async (caseItem) => { + if (!shouldFetch) { + return { + case: caseItem, + comments: [], + totalComments: caseItem.totalComment || 0, + }; + } + return fetchCommentsForCase(caseItem, casesClient, logger); + }) + ); +}; + +/** + * Enhances a case object with additional computed fields and formatting. + * Adds URL generation, markdown links, normalized assignees, observables summary, + * and comment summaries. Transforms user objects to usernames for display. + * + * @param caseItem - The base case object from the API + * @param comments - Array of comment summaries to include + * @param totalComments - Total number of comments for the case + * @param request - Kibana request object for URL generation + * @param coreServices - Core services including CoreStart and SpacesPlugin + * @param logger - Logger instance for warning messages + * @returns Enhanced case data object with all computed fields + */ +export const enhanceCaseData = ( + caseItem: Case, + comments: CommentSummary[], + totalComments: number, + request: KibanaRequest, + coreServices: CoreServices, + logger: Logger +): EnhancedCaseData => { + const caseUrl = + getCaseUrl( + request, + coreServices.coreStart, + coreServices.spacesPlugin, + caseItem.id, + caseItem.owner + ) || null; + if (!caseUrl) { + logger.warn( + `[Cases Tool] Failed to generate URL for case ${caseItem.id} with owner ${caseItem.owner}` + ); + } + + const markdownLink = caseUrl ? `[${caseItem.title}](${caseUrl})` : caseItem.title; + + return { + ...caseItem, + assignees: caseItem.assignees?.map((a) => a.uid || String(a)) || [], + observables_count: caseItem.total_observables ?? caseItem.observables?.length ?? 0, + observables: (caseItem.observables || []).slice(0, 5).map((obs) => ({ + type: obs.typeKey || null, + value: obs.value || null, + })), + created_by: caseItem.created_by.username ?? caseItem.created_by.email ?? null, + updated_by: caseItem.updated_by?.username ?? caseItem.updated_by?.email ?? null, + total_alerts: caseItem.totalAlerts || 0, + total_comments: totalComments, + comments_summary: comments, + url: caseUrl, + markdown_link: markdownLink, + }; +}; + +/** + * Creates a standardized tool result response for the cases tool. + * Formats the response according to the onechat tool result specification. + * + * @param cases - Array of enhanced case data objects + * @param timeRange - Normalized time range object or null if no time filtering + * @param message - Optional message to include in the response + * @returns Tool result object conforming to ToolResultType.other format + */ +export const createToolResult = ( + cases: EnhancedCaseData[], + timeRange: ReturnType | null, + message?: string +) => ({ + results: [ + { + type: ToolResultType.other, + data: { + total: cases.length, + cases, + start: timeRange?.start || null, + end: timeRange?.end || null, + ...(message && { message }), + }, + }, + ], +}); + +/** + * Enhances multiple cases with comments by applying enhanceCaseData to each. + * Processes all cases in the array and returns enhanced versions with URLs, + * markdown links, and formatted fields. + * + * @param casesWithComments - Array of CommentFetchResult objects containing cases and their comments + * @param coreServices - Core services including CoreStart and SpacesPlugin + * @param request - Kibana request object for URL generation + * @param logger - Logger instance for warning messages + * @returns Array of enhanced case data objects + */ +export const enhanceCasesWithComments = ( + casesWithComments: CommentFetchResult[], + coreServices: CoreServices, + request: KibanaRequest, + logger: Logger +): EnhancedCaseData[] => { + return casesWithComments.map(({ case: caseItem, comments, totalComments }) => + enhanceCaseData(caseItem, comments, totalComments, request, coreServices, logger) + ); +}; + +/** + * Creates a standardized error response for the cases tool. + * Extracts error message, logs it with the provided prefix, and returns + * a tool result with an error message formatted for the user. + * + * @param error - The error object of unknown type + * @param logPrefix - Prefix string for the error log message + * @param userMessage - User-friendly error message to display + * @param logger - Logger instance for error logging + * @returns Tool result object with error information + */ +export const createErrorResponse = ( + error: unknown, + logPrefix: string, + userMessage: string, + logger: Logger +) => { + const errorMessage = extractErrorMessage(error); + logger.error(`${logPrefix}: ${errorMessage}`); + return { + results: [createErrorResult(`${userMessage}: ${errorMessage}`)], + }; +}; + +/** + * Retrieves a cases client instance from the cases plugin. + * Checks if the cases plugin is available and creates a client scoped to the request. + * Returns an error result if the plugin is unavailable. + * + * @param pluginsStart - Plugin start dependencies containing the cases plugin + * @param request - Kibana request object for creating a scoped client + * @param logger - Logger instance for warning messages + * @param timeRange - Normalized time range for error responses (if plugin unavailable) + * @returns Promise resolving to either a cases client or an error result object + */ +export const getCasesClient = async ( + pluginsStart: OnechatStartDependencies, + request: KibanaRequest, + logger: Logger, + timeRange: ReturnType | null +): Promise<{ casesClient: CasesClient } | { error: ReturnType }> => { + const casesPlugin = pluginsStart.cases; + + if (!casesPlugin) { + logger.warn('[Cases Tool] Cases plugin not available, returning empty results'); + return { + error: createEmptyResults( + timeRange?.start || null, + timeRange?.end || null, + 'Cases plugin not available' + ), + }; + } + + const casesClient = await casesPlugin.getCasesClientWithRequest(request); + return { casesClient }; +}; + +/** + * Deduplicates cases across multiple arrays by case ID. + * Takes an array of case arrays (e.g., from multiple alert ID queries) + * and returns a single array with unique cases based on their ID. + * + * @param casesArrays - Array of arrays containing RelatedCase objects + * @returns Array of unique RelatedCase objects (first occurrence kept) + */ +export const deduplicateCases = (casesArrays: RelatedCase[][]): RelatedCase[] => { + const casesMap = new Map(); + for (const relatedCases of casesArrays) { + for (const relatedCase of relatedCases) { + if (!casesMap.has(relatedCase.id)) { + casesMap.set(relatedCase.id, relatedCase); + } + } + } + return Array.from(casesMap.values()); +}; From 2f49c6399c4efb74c2e97ca4034ecde6dfc9b356 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 15:20:02 -0700 Subject: [PATCH 26/96] agentBuilderEnabled --- .../server/services/tools/builtin/definitions/cases/cases.ts | 2 +- .../plugins/security_solution/common/experimental_features.ts | 4 ++++ .../security/plugins/security_solution/server/plugin.ts | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts index af945ef4adfea..88a6fdadc0ff7 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts @@ -365,7 +365,7 @@ Returns case details (id, title, description, status, severity, tags, assignees, allCases.push(...searchResult.cases); // Check if we should continue fetching - if (searchResult.cases.length < searchParams.perPage) { + if (searchResult.cases.length < (searchParams.perPage ?? 100)) { hasMorePages = false; } diff --git a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts index 13630894c231f..6a2851e9ff4bc 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/experimental_features.ts @@ -189,6 +189,10 @@ export const allowedExperimentalValues = Object.freeze({ * Enables the QRadar rules import feature */ qradarRulesMigration: false, + /** + * Enables dynamic registration of security attachments and tools from agent_builder with the onechat plugin + */ + agentBuilderEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 3f09a0ea3287a..258d29a0d2d79 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -619,7 +619,7 @@ export class Plugin implements ISecuritySolutionPlugin { // Note: This requires onechat to be added as an optional plugin dependency // Note: The alert attachment type may already be registered by onechat's built-in types. // If so, we'll skip registration and use the built-in version. - if (plugins.onechat) { + if (plugins.onechat && config.experimentalFeatures.agentBuilderEnabled) { try { // Register attachment type plugins.onechat.attachments.registerType(createAlertAttachmentType()); From 7ebc3c0f52a69b97bcfc159a27c2436ea4ea7d30 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 16:47:13 -0700 Subject: [PATCH 27/96] useAgentBuilderAttachment --- .../security_solution/common/constants.ts | 142 ++++++++++ .../plugins/security_solution/common/index.ts | 1 + .../new_agent_builder_attachment.tsx | 63 +++++ .../agent_builder/components/prompts.ts | 80 ++++++ .../agent_builder/components/translations.ts | 15 ++ .../hooks/use_agent_builder_attachment.ts | 200 ++++++++++++++ .../hooks/use_agent_builder_attachment.tsx | 244 ++++++++++++++++++ .../public/assistant/helpers.tsx | 17 ++ .../actionable_summary/index.tsx | 37 ++- .../tabs/attack_discovery_tab/index.tsx | 32 ++- .../view_in_ai_assistant/helpers.ts | 48 ++++ .../flyout/document_details/right/footer.tsx | 28 +- .../tools/alerts/alerts_index_search_tool.ts | 4 +- .../agent_builder/tools/alerts/alerts_tool.ts | 4 +- .../tools/alerts/evaluate_alert_tool.ts | 4 +- .../server/agent_builder/tools/constants.ts | 142 ---------- .../tools/entity_risk_score_tool.ts | 4 +- 17 files changed, 906 insertions(+), 159 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/translations.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/view_in_ai_assistant/helpers.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index c7a57adad554f..c7df2a5660de8 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -561,3 +561,145 @@ export const PROMOTION_RULE_TAGS = [ 'Promotion', // This is the legacy tag for promotion rules and can be safely removed once promotion rules go live 'Promotion: External Alerts', ]; + +/** + * Essential fields to return for security alerts to reduce context window usage. + * These fields contain the most relevant information for security analysis. + */ +export const ESSENTIAL_ALERT_FIELDS = [ + '_id', + '@timestamp', + 'message', + + /* Host */ + 'host.name', + 'host.ip', + 'host.os.name', + 'host.os.version', + 'host.asset.criticality', + 'host.risk.calculated_level', + 'host.risk.calculated_score_norm', + + /* User */ + 'user.name', + 'user.domain', + 'user.asset.criticality', + 'user.risk.calculated_level', + 'user.risk.calculated_score_norm', + 'user.target.name', + + /* Service */ + 'service.name', + 'service.id', + + /* Entity */ + 'entity.id', + 'entity.name', + 'entity.type', + 'entity.sub_type', + + /* Agent */ + 'agent.id', + + /* Process */ + 'process.name', + 'process.pid', + 'process.args', + 'process.command_line', + 'process.executable', + 'process.exit_code', + 'process.working_directory', + 'process.pe.original_file_name', + 'process.hash.md5', + 'process.hash.sha1', + 'process.hash.sha256', + 'process.code_signature.exists', + 'process.code_signature.signing_id', + 'process.code_signature.status', + 'process.code_signature.subject_name', + 'process.code_signature.trusted', + + /* Process parent */ + 'process.parent.name', + 'process.parent.args', + 'process.parent.args_count', + 'process.parent.command_line', + 'process.parent.executable', + 'process.parent.code_signature.exists', + 'process.parent.code_signature.status', + 'process.parent.code_signature.subject_name', + 'process.parent.code_signature.trusted', + + /* File */ + 'file.name', + 'file.path', + 'file.Ext.original.path', + 'file.hash.sha256', + + /* Groups */ + 'group.id', + 'group.name', + + /* Cloud */ + 'cloud.provider', + 'cloud.account.name', + 'cloud.service.name', + 'cloud.region', + 'cloud.availability_zone', + + /* Network / DNS */ + 'source.ip', + 'destination.ip', + 'network.protocol', + 'dns.question.name', + 'dns.question.type', + + /* Event */ + 'event.category', + 'event.action', + 'event.type', + 'event.code', + 'event.dataset', + 'event.module', + 'event.outcome', + + /* Rule (generic) */ + 'rule.name', + 'rule.reference', + + /* Kibana alert fields */ + 'kibana.alert.uuid', + 'kibana.alert.original_time', + 'kibana.alert.severity', + 'kibana.alert.start', + 'kibana.alert.workflow_status', + 'kibana.alert.reason', + 'kibana.alert.risk_score', + 'kibana.alert.rule.name', + 'kibana.alert.rule.rule_id', + 'kibana.alert.rule.description', + 'kibana.alert.rule.category', + 'kibana.alert.rule.references', + 'kibana.alert.rule.threat.framework', + 'kibana.alert.rule.threat.tactic.id', + 'kibana.alert.rule.threat.tactic.name', + 'kibana.alert.rule.threat.tactic.reference', + 'kibana.alert.rule.threat.technique.id', + 'kibana.alert.rule.threat.technique.name', + 'kibana.alert.rule.threat.technique.reference', + 'kibana.alert.rule.threat.technique.subtechnique.id', + 'kibana.alert.rule.threat.technique.subtechnique.name', + 'kibana.alert.rule.threat.technique.subtechnique.reference', + + /* Threat (top-level) */ + 'threat.framework', + 'threat.tactic.id', + 'threat.tactic.name', + 'threat.tactic.reference', + 'threat.technique.id', + 'threat.technique.name', + 'threat.technique.reference', + 'threat.technique.subtechnique.id', + 'threat.technique.subtechnique.name', + 'threat.technique.subtechnique.reference', +] as const; diff --git a/x-pack/solutions/security/plugins/security_solution/common/index.ts b/x-pack/solutions/security/plugins/security_solution/common/index.ts index 7bd4c019a6d15..d1ce693e39c38 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/index.ts @@ -16,6 +16,7 @@ export { ADD_DATA_PATH, SecurityPageName, DETECTION_ENGINE_RULES_URL_FIND, + ESSENTIAL_ALERT_FIELDS, } from './constants'; export { ELASTIC_SECURITY_RULE_ID } from './detection_engine/constants'; export { ENABLED_FIELD } from './detection_engine/rule_management/rule_fields'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx new file mode 100644 index 0000000000000..89bcc680529d1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx @@ -0,0 +1,63 @@ +/* + * 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. + */ + +import type { EuiButtonColor } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import React from 'react'; +import type { EuiButtonEmptySizes } from '@elastic/eui/src/components/button/button_empty/button_empty'; +import * as i18n from './translations'; + +export const BUTTON_TEST_ID = 'newAgentBuilderAttachment'; +export const BUTTON_ICON_TEST_ID = 'newAgentBuilderAttachmentIcon'; +export const BUTTON_TEXT_TEST_ID = 'newAgentBuilderAttachmentText'; + +export interface NewAgentBuilderAttachmentProps { + /** + * Optionally specify color of empty button. + * @default 'primary' + */ + color?: EuiButtonColor; + /** + * Callback when button is clicked + */ + onClick: () => void; + /** + * Size of the button + */ + size?: EuiButtonEmptySizes; +} + +const NewAgentBuilderAttachmentComponent: React.FC = ({ + color = 'primary', + onClick, + size = 'm', +}) => { + return ( + + + + + + {i18n.VIEW_IN_AGENT_BUILDER} + + + ); +}; + +NewAgentBuilderAttachmentComponent.displayName = 'NewAgentBuilderAttachmentComponent'; + +/** + * `NewAgentBuilderAttachment` displays a button that opens the agent builder flyout + * with attachment data. You may optionally override the default text. + */ +export const NewAgentBuilderAttachment = React.memo(NewAgentBuilderAttachmentComponent); diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts new file mode 100644 index 0000000000000..60456b01d91ac --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts @@ -0,0 +1,80 @@ +/* + * 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. + */ + +export const ATTACK_DISCOVERY_ATTACHMENT_PROMPT = `Summarize the attack discovery attached and recommend next steps. Case URLs MUST be included in the response if they exist. Summary should be in markdown.`; +export const ALERT_ATTACHMENT_PROMPT = `Evaluate the provided security alert and generate a structured, markdown-formatted summary suitable for inclusion in an Elastic Security case. Use all available enrichment tools before generating your response. Include the following sections: + +--- + +## 1. Event Description 📝 + +* Summarize the alert using extracted data: + + * **Alert ID**: \`kibana.alert.uuid\` or \`_id\` + * **Rule Name**: \`kibana.alert.rule.name\` + * **Entities**: \`host.name\`, \`user.name\`, \`service.name\` + * Include associated **risk scores** for each entity (from the risk score tool) + * Reference **MITRE ATT&CK techniques** with links (\`kibana.alert.rule.threat.technique.id\`, \`kibana.alert.rule.threat.tactic.id\`, \`threat.tactic.id\`) + +--- + +## 2. Associated Cases & Attack Discoveries 🔍 + +* Summarize any attack discoveries that include this alert ID, highlighting: + + * Involved hosts, users, and status + * Patterns or recurring behaviors + +* List all open or related security cases referencing this alert ID, **always using markdown links** to the case URLs (from the cases tool) + +--- + +## 3. Triage Steps 🛡️ + +* Provide clear, actionable triage steps tailored to Elastic Security workflows: + + * Consider the alert’s rule, involved entities, and MITRE context + * Include relevant detection rules or anomaly findings + * Reference Security Labs articles related to the MITRE technique or alert rule (with links) + +--- + +## 4. Recommended Actions ⚡ + +* Prioritized response actions using enriched context: + + * **Elastic Defend endpoint actions** (e.g., isolate host, kill process, retrieve/delete file) with documentation links + * **Example queries for further investigation**: + + * Elasticsearch / EQL queries (code blocks) + * OSQuery Manager queries (code blocks) + + * Guidance for using **Timelines** and **Entity Analytics** for deeper context (with documentation links) + +--- + +## 5. MITRE ATT&CK Context 📊 + +* Summarize mapped MITRE ATT&CK techniques +* Provide actionable recommendations based on MITRE guidance, including hyperlinks + +--- + +## 6. Documentation Links 📚 + +* Include direct links to all referenced Elastic Security documentation, Security Labs articles, and MITRE ATT&CK pages + +--- + +**Formatting Requirements** + +* Use markdown headers, tables, and code blocks for clarity +* Organize sections visually and consistently +* Use concise, actionable language +* Include emojis in section headers for clarity + +**CRITICAL:** You MUST incorporate results from **all enrichment tools** (risk scores, attack discoveries, related cases, Security Labs) before generating the response. Do not skip any step.`; diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/translations.ts new file mode 100644 index 0000000000000..25edcb97d0ecd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/translations.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +import { i18n } from '@kbn/i18n'; + +export const VIEW_IN_AGENT_BUILDER = i18n.translate( + 'xpack.securitySolution.agentBuilder.viewInAgentBuilderButtonLabel', + { + defaultMessage: 'View in Agent Builder', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts new file mode 100644 index 0000000000000..e278db33acd1e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts @@ -0,0 +1,200 @@ +/* + * 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. + */ + +import { useCallback, useState } from 'react'; +import React from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiText, + logicalCSS, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { useAppToasts } from '../../common/hooks/use_app_toasts'; +import { useAppUrl } from '../../common/lib/kibana/hooks'; + +export interface UseAgentBuilderAttachmentParams { + /** + * Type of attachment (e.g., 'alert', 'attack_discovery') + */ + attachmentType: string; + /** + * Data for the attachment + */ + attachmentData: Record; + /** + * Prompt/input text for the agent builder conversation + */ + attachmentPrompt: string; +} + +export interface UseAgentBuilderAttachmentResult { + /** + * Function to open the agent builder flyout + * TODO: This currently calls the API directly as a temporary implementation. + * Once the agent builder UI is ready, this will open a flyout with the attachment data instead. + */ + openAgentBuilderFlyout: () => void; + /** + * Whether the API call is in progress + */ + isLoading: boolean; +} + +const AgentBuilderToastSuccessContent: React.FC<{ + onViewConversationClick?: () => void; + content?: string; +}> = ({ onViewConversationClick, content }) => { + const { euiTheme } = useEuiTheme(); + return React.createElement( + React.Fragment, + null, + content !== undefined + ? React.createElement( + EuiText, + { + size: 's', + css: css` + ${logicalCSS('margin-bottom', euiTheme.size.s)}; + `, + 'data-test-subj': 'toaster-content-sync-text', + }, + content + ) + : null, + onViewConversationClick !== undefined + ? React.createElement( + EuiFlexGroup, + { justifyContent: 'flexEnd', gutterSize: 's' }, + React.createElement( + EuiFlexItem, + { grow: false }, + React.createElement( + EuiButton, + { + size: 's', + onClick: onViewConversationClick, + 'data-test-subj': 'toaster-content-conversation-view-link', + }, + i18n.translate( + 'xpack.securitySolution.agentBuilder.attachment.viewConversationButton', + { + defaultMessage: 'View conversation', + } + ) + ) + ) + ) + : null + ); +}; + +/** + * Hook to handle agent builder attachment functionality. + * Temporarily calls the API directly until the agent builder UI is ready. + * Eventually, this will open a flyout with the attachment data. + */ +export const useAgentBuilderAttachment = ({ + attachmentType, + attachmentData, + attachmentPrompt, +}: UseAgentBuilderAttachmentParams): UseAgentBuilderAttachmentResult => { + const { http, application, i18n: i18nService, theme, userProfile } = useKibana().services; + const toasts = useAppToasts(); + const { getAppUrl } = useAppUrl(); + const [isLoading, setIsLoading] = useState(false); + + const openAgentBuilderFlyout = useCallback(async () => { + setIsLoading(true); + try { + // TODO: This API call is temporary until the agent builder UI is ready. + // Once the UI is ready, this will open a flyout with the attachment data instead. + const result = await http.post('/api/agent_builder/converse', { + body: JSON.stringify({ + input: attachmentPrompt, + attachments: [ + { + type: attachmentType, + data: attachmentData, + }, + ], + }), + version: '2023-10-31', + }); + + const conversationId = result?.conversation_id; + const conversationUrl = conversationId + ? getAppUrl({ + appId: 'agent_builder', + path: `/conversations/${conversationId}`, + }) + : null; + + const onViewConversationClick = () => { + if (conversationUrl) { + application.navigateToUrl(conversationUrl); + } + }; + + const renderContent = i18n.translate( + 'xpack.securitySolution.agentBuilder.attachment.successText', + { + defaultMessage: 'Your attachment has been sent to the agent builder.', + } + ); + + toasts.addSuccess({ + color: 'success', + iconType: 'check', + title: i18n.translate('xpack.securitySolution.agentBuilder.attachment.successTitle', { + defaultMessage: 'Agent builder conversation started', + }), + text: conversationUrl + ? toMountPoint( + React.createElement(AgentBuilderToastSuccessContent, { + content: renderContent, + onViewConversationClick: onViewConversationClick, + }), + { i18n: i18nService, theme, userProfile } + ) + : renderContent, + }); + } catch (error) { + toasts.addError(error, { + title: i18n.translate('xpack.securitySolution.agentBuilder.attachment.errorTitle', { + defaultMessage: 'Failed to start agent builder conversation', + }), + toastMessage: i18n.translate('xpack.securitySolution.agentBuilder.attachment.errorText', { + defaultMessage: 'There was an error sending your attachment to the agent builder.', + }), + }); + } finally { + setIsLoading(false); + } + }, [ + attachmentType, + attachmentData, + attachmentPrompt, + http, + toasts, + getAppUrl, + application, + i18nService, + theme, + userProfile, + ]); + + return { + openAgentBuilderFlyout, + isLoading, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.tsx new file mode 100644 index 0000000000000..c2367c6c8df27 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.tsx @@ -0,0 +1,244 @@ +/* + * 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. + */ + +import React, { useCallback, useState, useRef, useEffect } from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiText, + logicalCSS, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { useIsMounted } from '@kbn/securitysolution-hook-utils'; +import { useAppToasts } from '../../common/hooks/use_app_toasts'; +import { useAppUrl } from '../../common/lib/kibana/hooks'; + +export interface UseAgentBuilderAttachmentParams { + /** + * Type of attachment (e.g., 'alert', 'attack_discovery') + */ + attachmentType: string; + /** + * Data for the attachment + */ + attachmentData: Record; + /** + * Prompt/input text for the agent builder conversation + */ + attachmentPrompt: string; +} + +export interface UseAgentBuilderAttachmentResult { + /** + * Function to open the agent builder flyout + * TODO: This currently calls the API directly as a temporary implementation. + * Once the agent builder UI is ready, this will open a flyout with the attachment data instead. + */ + openAgentBuilderFlyout: () => void; + /** + * Whether the API call is in progress + */ + isLoading: boolean; +} + +/** + * Hook to handle agent builder attachment functionality. + * Temporarily calls the API directly until the agent builder UI is ready. + * Eventually, this will open a flyout with the attachment data. + * + * Improvements over previous implementation: + * - Uses useIsMounted to prevent state updates after component unmounts + * - Implements abort controller for request cancellation + * - Prevents race conditions by cancelling previous requests when a new one starts + * - Proper cleanup on unmount + */ +export const useAgentBuilderAttachment = ({ + attachmentType, + attachmentData, + attachmentPrompt, +}: UseAgentBuilderAttachmentParams): UseAgentBuilderAttachmentResult => { + const { http, application, i18n: i18nService, theme, userProfile } = useKibana().services; + const toasts = useAppToasts(); + const { getAppUrl } = useAppUrl(); + const [isLoading, setIsLoading] = useState(false); + const isMounted = useIsMounted(); + const abortControllerRef = useRef(null); + + // Cleanup abort controller on unmount + useEffect(() => { + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + }; + }, []); + + const openAgentBuilderFlyout = useCallback(async () => { + // Cancel any previous request + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + // Create new abort controller for this request + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + // Only update loading state if component is still mounted + if (!isMounted()) { + return; + } + setIsLoading(true); + + try { + // TODO: This API call is temporary until the agent builder UI is ready. + // Once the UI is ready, this will open a flyout with the attachment data instead. + const result = await http.post('/api/agent_builder/converse', { + body: JSON.stringify({ + input: attachmentPrompt, + attachments: [ + { + type: attachmentType, + data: attachmentData, + }, + ], + }), + version: '2023-10-31', + signal: abortController.signal, + }); + + // Check if request was aborted or component unmounted + if (abortController.signal.aborted || !isMounted()) { + return; + } + + const conversationId = result?.conversation_id; + const conversationUrl = conversationId + ? getAppUrl({ + appId: 'agent_builder', + path: `/conversations/${conversationId}`, + }) + : null; + + const onViewConversationClick = () => { + if (conversationUrl) { + application.navigateToUrl(conversationUrl); + } + }; + + const renderContent = i18n.translate( + 'xpack.securitySolution.agentBuilder.attachment.successText', + { + defaultMessage: 'Your attachment has been sent to the agent builder.', + } + ); + + toasts.addSuccess({ + color: 'success', + iconType: 'check', + title: i18n.translate('xpack.securitySolution.agentBuilder.attachment.successTitle', { + defaultMessage: 'Agent builder conversation started', + }), + text: conversationUrl + ? toMountPoint( + , + { i18n: i18nService, theme, userProfile } + ) + : renderContent, + }); + } catch (error) { + // Don't show error toast if request was aborted or component unmounted + if (abortController.signal.aborted || !isMounted()) { + return; + } + + toasts.addError(error, { + title: i18n.translate('xpack.securitySolution.agentBuilder.attachment.errorTitle', { + defaultMessage: 'Failed to start agent builder conversation', + }), + toastMessage: i18n.translate('xpack.securitySolution.agentBuilder.attachment.errorText', { + defaultMessage: 'There was an error sending your attachment to the agent builder.', + }), + }); + } finally { + // Only update loading state if this request wasn't aborted and component is still mounted + if (!abortController.signal.aborted && isMounted()) { + setIsLoading(false); + } + + // Clear the abort controller ref if this was the current request + if (abortControllerRef.current === abortController) { + abortControllerRef.current = null; + } + } + }, [ + attachmentType, + attachmentData, + attachmentPrompt, + http, + toasts, + getAppUrl, + application, + i18nService, + theme, + userProfile, + isMounted, + ]); + + return { + openAgentBuilderFlyout, + isLoading, + }; +}; + +const AgentBuilderToastSuccessContent: React.FC<{ + onViewConversationClick?: () => void; + content?: string; +}> = ({ onViewConversationClick, content }) => { + const { euiTheme } = useEuiTheme(); + return ( + <> + {content !== undefined ? ( + + {content} + + ) : null} + {onViewConversationClick !== undefined ? ( + + + + {i18n.translate( + 'xpack.securitySolution.agentBuilder.attachment.viewConversationButton', + { + defaultMessage: 'View conversation', + } + )} + + + + ) : null} + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx index 4267dc38eeedc..5637397f503a8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx @@ -8,6 +8,7 @@ import type { CodeBlockDetails } from '@kbn/elastic-assistant'; import type { TimelineEventsDetailsItem } from '../../common/search_strategy'; import type { Rule } from '../detection_engine/rule_management/logic'; +import { ESSENTIAL_ALERT_FIELDS } from '../../common/constants'; export const LOCAL_STORAGE_KEY = `securityAssistant`; @@ -22,6 +23,22 @@ export const getRawData = (data: TimelineEventsDetailsItem[]): Record !field.startsWith('signal.')) .reduce((acc, { field, values }) => ({ ...acc, [field]: values ?? [] }), {}); +/** + * Filters raw alert data to only include essential fields and stringifies the result. + * This reduces context window usage by keeping only the most relevant information. + */ +export const filterAndStringifyAlertData = (rawData: Record): string => { + const essentialFieldsSet = new Set(ESSENTIAL_ALERT_FIELDS); + const filteredData = Object.keys(rawData) + .filter((key) => essentialFieldsSet.has(key)) + .reduce((acc, key) => { + acc[key] = rawData[key]; + return acc; + }, {} as Record); + + return JSON.stringify(filteredData); +}; + export const sendToTimelineEligibleQueryTypes: Array = [ 'kql', 'dsl', diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx index 6b75eaba8c1cc..06315feced1bf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx @@ -8,15 +8,21 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { type AttackDiscovery, + getAttackDiscoveryMarkdown, replaceAnonymizedValuesWithOriginalValues, type Replacements, } from '@kbn/elastic-assistant-common'; +import { AttachmentType } from '@kbn/onechat-common/attachments'; import React, { useMemo } from 'react'; +import { ATTACK_DISCOVERY_ATTACHMENT_PROMPT } from '../../../../../agent_builder/components/prompts'; import { SECURITY_FEATURE_ID } from '../../../../../../common'; import { useKibana } from '../../../../../common/lib/kibana'; import { AttackDiscoveryMarkdownFormatter } from '../../attack_discovery_markdown_formatter'; import { ViewInAiAssistant } from '../view_in_ai_assistant'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { NewAgentBuilderAttachment } from '../../../../../agent_builder/components/new_agent_builder_attachment'; +import { useAgentBuilderAttachment } from '../../../../../agent_builder/hooks/use_agent_builder_attachment'; interface Props { attackDiscovery: AttackDiscovery; @@ -68,6 +74,23 @@ const ActionableSummaryComponent: React.FC = ({ const entitySummaryOrTitle = entitySummary != null && entitySummary.length > 0 ? entitySummary : title; + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); + const attackDiscoveryWithOriginalValues = useMemo( + () => + // Agent builder is not anonymized + getAttackDiscoveryMarkdown({ + attackDiscovery, + replacements, + }), + [attackDiscovery, replacements] + ); + + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ + attachmentType: AttachmentType.attack_discovery, + attachmentData: { attackDiscovery: attackDiscoveryWithOriginalValues }, + attachmentPrompt: ATTACK_DISCOVERY_ATTACHMENT_PROMPT, + }); + return ( @@ -79,11 +102,15 @@ const ActionableSummaryComponent: React.FC = ({ - + {isAgentBuilderEnabled ? ( + + ) : ( + + )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx index 0815c653ec637..648d0764eadbc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx @@ -6,11 +6,16 @@ */ import type { AttackDiscovery, Replacements } from '@kbn/elastic-assistant-common'; -import { replaceAnonymizedValuesWithOriginalValues } from '@kbn/elastic-assistant-common'; +import { + getAttackDiscoveryMarkdown, + replaceAnonymizedValuesWithOriginalValues, +} from '@kbn/elastic-assistant-common'; +import { AttachmentType } from '@kbn/onechat-common/attachments'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiTitle, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import React, { useMemo } from 'react'; +import { ATTACK_DISCOVERY_ATTACHMENT_PROMPT } from '../../../../../../agent_builder/components/prompts'; import { useKibana } from '../../../../../../common/lib/kibana'; import { AttackChain } from './attack/attack_chain'; import { InvestigateInTimelineButton } from '../../../../../../common/components/event_details/investigate_in_timeline_button'; @@ -20,6 +25,9 @@ import { AttackDiscoveryMarkdownFormatter } from '../../../attack_discovery_mark import * as i18n from './translations'; import { ViewInAiAssistant } from '../../view_in_ai_assistant'; import { SECURITY_FEATURE_ID } from '../../../../../../../common'; +import { useIsExperimentalFeatureEnabled } from '../../../../../../common/hooks/use_experimental_features'; +import { NewAgentBuilderAttachment } from '../../../../../../agent_builder/components/new_agent_builder_attachment'; +import { useAgentBuilderAttachment } from '../../../../../../agent_builder/hooks/use_agent_builder_attachment'; const scrollable = css` overflow-x: auto; @@ -78,6 +86,22 @@ const AttackDiscoveryTabComponent: React.FC = ({ const filters = useMemo(() => buildAlertsKqlFilter('_id', originalAlertIds), [originalAlertIds]); + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); + const attackDiscoveryWithOriginalValues = useMemo( + () => + getAttackDiscoveryMarkdown({ + attackDiscovery, + replacements, + }), + [attackDiscovery, replacements] + ); + + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ + attachmentType: AttachmentType.attack_discovery, + attachmentData: { attackDiscovery: attackDiscoveryWithOriginalValues }, + attachmentPrompt: ATTACK_DISCOVERY_ATTACHMENT_PROMPT, + }); + return (
@@ -120,7 +144,11 @@ const AttackDiscoveryTabComponent: React.FC = ({ - + {isAgentBuilderEnabled ? ( + + ) : ( + + )} { + if (!replacements) { + return attackDiscovery; + } + + return { + ...attackDiscovery, + detailsMarkdown: replaceAnonymizedValuesWithOriginalValues({ + messageContent: attackDiscovery.detailsMarkdown, + replacements, + }), + entitySummaryMarkdown: attackDiscovery.entitySummaryMarkdown + ? replaceAnonymizedValuesWithOriginalValues({ + messageContent: attackDiscovery.entitySummaryMarkdown, + replacements, + }) + : undefined, + summaryMarkdown: replaceAnonymizedValuesWithOriginalValues({ + messageContent: attackDiscovery.summaryMarkdown, + replacements, + }), + title: replaceAnonymizedValuesWithOriginalValues({ + messageContent: attackDiscovery.title, + replacements, + }), + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx index b617f52e94937..9b1f3525598a7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx @@ -6,15 +6,20 @@ */ import type { FC } from 'react'; -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NewChatByTitle } from '@kbn/elastic-assistant'; +import { ALERT_ATTACHMENT_PROMPT } from '../../../agent_builder/components/prompts'; import { useBasicDataFromDetailsData } from '../shared/hooks/use_basic_data_from_details_data'; import { useDocumentDetailsContext } from '../shared/context'; import { useAssistant } from './hooks/use_assistant'; import { FLYOUT_FOOTER_TEST_ID } from './test_ids'; import { TakeActionButton } from '../shared/components/take_action_button'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import { NewAgentBuilderAttachment } from '../../../agent_builder/components/new_agent_builder_attachment'; +import { useAgentBuilderAttachment } from '../../../agent_builder/hooks/use_agent_builder_attachment'; +import { getRawData, filterAndStringifyAlertData } from '../../../assistant/helpers'; export const ASK_AI_ASSISTANT = i18n.translate( 'xpack.securitySolution.ease.flyout.right.footer.askAIAssistant', @@ -40,6 +45,18 @@ export const PanelFooter: FC = ({ isRulePreview }) => { dataFormattedForFieldBrowser, isAlert, }); + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); + + const alertData = useMemo(() => { + const rawData = getRawData(dataFormattedForFieldBrowser ?? []); + return filterAndStringifyAlertData(rawData); + }, [dataFormattedForFieldBrowser]); + + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ + attachmentType: 'alert', + attachmentData: { alert: alertData }, + attachmentPrompt: ALERT_ATTACHMENT_PROMPT, + }); if (isRulePreview) return null; @@ -49,7 +66,14 @@ export const PanelFooter: FC = ({ isRulePreview }) => { {showAssistant && ( - + {isAgentBuilderEnabled ? ( + + ) : ( + + )} )} diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts index 18cd49c8f8b43..e5e04361c7bce 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts @@ -9,9 +9,9 @@ import { z } from '@kbn/zod'; import { ToolType } from '@kbn/onechat-common'; import { generateEsql } from '@kbn/onechat-genai-utils/tools'; import type { BuiltinToolDefinition } from '@kbn/onechat-server'; -import { DEFAULT_ALERTS_INDEX } from '../../../../common/constants'; +import { DEFAULT_ALERTS_INDEX, ESSENTIAL_ALERT_FIELDS } from '../../../../common/constants'; import { getSpaceIdFromRequest } from '../helpers'; -import { ESSENTIAL_ALERT_FIELDS, securityTool } from '../constants'; +import { securityTool } from '../constants'; const alertsIndexSearchSchema = z.object({ query: z diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts index d1601f42e39a9..f6a4797a5050c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts @@ -9,9 +9,9 @@ import { z } from '@kbn/zod'; import { ToolType } from '@kbn/onechat-common'; import { generateEsql } from '@kbn/onechat-genai-utils/tools'; import type { BuiltinToolDefinition } from '@kbn/onechat-server'; -import { DEFAULT_ALERTS_INDEX } from '../../../../common/constants'; +import { DEFAULT_ALERTS_INDEX, ESSENTIAL_ALERT_FIELDS } from '../../../../common/constants'; import { getSpaceIdFromRequest } from '../helpers'; -import { ESSENTIAL_ALERT_FIELDS, securityTool } from '../constants'; +import { securityTool } from '../constants'; const alertsSchema = z.object({ query: z diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts index 6baefc558cede..a738e55307d6e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts @@ -14,10 +14,10 @@ import type { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; import { generateEsql } from '@kbn/onechat-genai-utils/tools'; import { runSearchTool } from '@kbn/onechat-genai-utils/tools/search/run_search_tool'; import { ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX } from '@kbn/elastic-assistant-common'; -import { DEFAULT_ALERTS_INDEX } from '../../../../common/constants'; +import { DEFAULT_ALERTS_INDEX, ESSENTIAL_ALERT_FIELDS } from '../../../../common/constants'; import { getRiskIndex } from '../../../../common/search_strategy/security_solution/risk_score/common'; import { getSpaceIdFromRequest } from '../helpers'; -import { ESSENTIAL_ALERT_FIELDS, securityTool } from '../constants'; +import { securityTool } from '../constants'; const evaluateAlertSchema = z.object({ alertData: z diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts index 7f9b3d13bc8ce..b4645048ad03b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts @@ -13,145 +13,3 @@ import { internalNamespaces } from '@kbn/onechat-common/base/namespaces'; export const securityTool = (toolName: string): string => { return `${internalNamespaces.coreSecurity}.${toolName}`; }; - -/** - * Essential fields to return for security alerts to reduce context window usage. - * These fields contain the most relevant information for security analysis. - */ -export const ESSENTIAL_ALERT_FIELDS = [ - '_id', - '@timestamp', - 'message', - - /* Host */ - 'host.name', - 'host.ip', - 'host.os.name', - 'host.os.version', - 'host.asset.criticality', - 'host.risk.calculated_level', - 'host.risk.calculated_score_norm', - - /* User */ - 'user.name', - 'user.domain', - 'user.asset.criticality', - 'user.risk.calculated_level', - 'user.risk.calculated_score_norm', - 'user.target.name', - - /* Service */ - 'service.name', - 'service.id', - - /* Entity */ - 'entity.id', - 'entity.name', - 'entity.type', - 'entity.sub_type', - - /* Agent */ - 'agent.id', - - /* Process */ - 'process.name', - 'process.pid', - 'process.args', - 'process.command_line', - 'process.executable', - 'process.exit_code', - 'process.working_directory', - 'process.pe.original_file_name', - 'process.hash.md5', - 'process.hash.sha1', - 'process.hash.sha256', - 'process.code_signature.exists', - 'process.code_signature.signing_id', - 'process.code_signature.status', - 'process.code_signature.subject_name', - 'process.code_signature.trusted', - - /* Process parent */ - 'process.parent.name', - 'process.parent.args', - 'process.parent.args_count', - 'process.parent.command_line', - 'process.parent.executable', - 'process.parent.code_signature.exists', - 'process.parent.code_signature.status', - 'process.parent.code_signature.subject_name', - 'process.parent.code_signature.trusted', - - /* File */ - 'file.name', - 'file.path', - 'file.Ext.original.path', - 'file.hash.sha256', - - /* Groups */ - 'group.id', - 'group.name', - - /* Cloud */ - 'cloud.provider', - 'cloud.account.name', - 'cloud.service.name', - 'cloud.region', - 'cloud.availability_zone', - - /* Network / DNS */ - 'source.ip', - 'destination.ip', - 'network.protocol', - 'dns.question.name', - 'dns.question.type', - - /* Event */ - 'event.category', - 'event.action', - 'event.type', - 'event.code', - 'event.dataset', - 'event.module', - 'event.outcome', - - /* Rule (generic) */ - 'rule.name', - 'rule.reference', - - /* Kibana alert fields */ - 'kibana.alert.uuid', - 'kibana.alert.original_time', - 'kibana.alert.severity', - 'kibana.alert.start', - 'kibana.alert.workflow_status', - 'kibana.alert.reason', - 'kibana.alert.risk_score', - 'kibana.alert.rule.name', - 'kibana.alert.rule.rule_id', - 'kibana.alert.rule.description', - 'kibana.alert.rule.category', - 'kibana.alert.rule.references', - 'kibana.alert.rule.threat.framework', - 'kibana.alert.rule.threat.tactic.id', - 'kibana.alert.rule.threat.tactic.name', - 'kibana.alert.rule.threat.tactic.reference', - 'kibana.alert.rule.threat.technique.id', - 'kibana.alert.rule.threat.technique.name', - 'kibana.alert.rule.threat.technique.reference', - 'kibana.alert.rule.threat.technique.subtechnique.id', - 'kibana.alert.rule.threat.technique.subtechnique.name', - 'kibana.alert.rule.threat.technique.subtechnique.reference', - - /* Threat (top-level) */ - 'threat.framework', - 'threat.tactic.id', - 'threat.tactic.name', - 'threat.tactic.reference', - 'threat.technique.id', - 'threat.technique.name', - 'threat.technique.reference', - 'threat.technique.subtechnique.id', - 'threat.technique.subtechnique.name', - 'threat.technique.subtechnique.reference', -] as const; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts index 65e090e2faba5..45e147192e54d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts @@ -13,9 +13,9 @@ import { getToolResultId } from '@kbn/onechat-server/tools'; import { IdentifierType } from '../../../common/api/entity_analytics/common/common.gen'; import { createGetRiskScores } from '../../lib/entity_analytics/risk_score/get_risk_score'; import type { EntityType } from '../../../common/entity_analytics/types'; -import { DEFAULT_ALERTS_INDEX } from '../../../common/constants'; +import { DEFAULT_ALERTS_INDEX, ESSENTIAL_ALERT_FIELDS } from '../../../common/constants'; import { getSpaceIdFromRequest } from './helpers'; -import { ESSENTIAL_ALERT_FIELDS, securityTool } from './constants'; +import { securityTool } from './constants'; const entityRiskScoreSchema = z.object({ identifierType: IdentifierType.describe('The type of entity: host, user, service, or generic'), From 348b50402d6f4c4f14b910e75e3ff18fcb59bb13 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 16:59:01 -0700 Subject: [PATCH 28/96] add risk entity --- .../attachments/attachment_types.ts | 17 ++++++ .../onechat-common/attachments/attachments.ts | 1 + .../onechat-common/attachments/index.ts | 3 + .../attachments/attack_discovery.ts | 2 +- .../server/agent_builder/attachments/index.ts | 1 + .../agent_builder/attachments/risk_entity.ts | 60 +++++++++++++++++++ .../security_solution/server/plugin.ts | 15 +++++ 7 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts index 3a6316de672ae..812143b9c6be7 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts @@ -18,6 +18,7 @@ export enum AttachmentType { esql = 'esql', alert = 'alert', attack_discovery = 'attack_discovery', + risk_entity = 'risk_entity', } interface AttachmentDataMap { @@ -26,6 +27,7 @@ interface AttachmentDataMap { [AttachmentType.screenContext]: ScreenContextAttachmentData; [AttachmentType.alert]: AlertAttachmentData; [AttachmentType.attack_discovery]: AttackDiscoveryAttachmentData; + [AttachmentType.risk_entity]: RiskEntityAttachmentData; } export const esqlAttachmentDataSchema = z.object({ @@ -105,4 +107,19 @@ export interface AttackDiscoveryAttachmentData { attackDiscovery: string; } +export const riskEntityAttachmentDataSchema = z.object({ + identifierType: z.enum(['host', 'user', 'service', 'generic']), + identifier: z.string().min(1), +}); + +/** + * Data for a risk entity attachment. + */ +export interface RiskEntityAttachmentData { + /** The type of entity: host, user, service, or generic */ + identifierType: 'host' | 'user' | 'service' | 'generic'; + /** The value that identifies the entity (e.g., hostname, username) */ + identifier: string; +} + export type AttachmentDataOf = AttachmentDataMap[Type]; diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts index 1f8e1e4aa22d8..fcc1e8da01f5d 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts @@ -31,6 +31,7 @@ export type ScreenContextAttachment = Attachment; export type EsqlAttachment = Attachment; export type AlertAttachment = Attachment; export type AttackDiscoveryAttachment = Attachment; +export type RiskEntityAttachment = Attachment; /** * Input version of an attachment, where the id is optional diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts index 8295d33900104..0aff3ba88779b 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts @@ -13,6 +13,7 @@ export type { EsqlAttachment, AlertAttachment, AttackDiscoveryAttachment, + RiskEntityAttachment, } from './attachments'; export { AttachmentType, @@ -21,9 +22,11 @@ export { screenContextAttachmentDataSchema, alertAttachmentDataSchema, attackDiscoveryAttachmentDataSchema, + riskEntityAttachmentDataSchema, type TextAttachmentData, type ScreenContextAttachmentData, type EsqlAttachmentData, type AlertAttachmentData, type AttackDiscoveryAttachmentData, + type RiskEntityAttachmentData, } from './attachment_types'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts index 55ad7e8976c71..879d86c6f4e46 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts @@ -66,7 +66,7 @@ MANDATORY WORKFLOW - Complete in order: - Look for alert IDs referenced in the attack discovery - These may be mentioned in the details or summary sections -4. Query RISK SCORES for entities: +4. Query ENTITY RISK SCORE for entities: Tool: ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} Parameters: { identifierType: "host.name", identifier: "[host name]" } Repeat for each unique host.name, user.name found diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts index dc411935843f2..90097a81be92e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts @@ -7,4 +7,5 @@ export { createAlertAttachmentType } from './alert'; export { createAttackDiscoveryAttachmentType } from './attack_discovery'; +export { createRiskEntityAttachmentType } from './risk_entity'; // export { createAlertAttachmentType } from './core-alert'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts new file mode 100644 index 0000000000000..88b410e348e6a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts @@ -0,0 +1,60 @@ +/* + * 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. + */ + +import type { RiskEntityAttachmentData } from '@kbn/onechat-common/attachments'; +import { AttachmentType, riskEntityAttachmentDataSchema } from '@kbn/onechat-common/attachments'; +import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; +import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import { SECURITY_ENTITY_RISK_SCORE_TOOL_ID } from '../tools'; + +/** + * Creates the definition for the `risk_entity` attachment type. + */ +export const createRiskEntityAttachmentType = (): AttachmentTypeDefinition< + AttachmentType.risk_entity, + RiskEntityAttachmentData +> => { + return { + id: AttachmentType.risk_entity, + validate: (input) => { + const parseResult = riskEntityAttachmentDataSchema.safeParse(input); + if (parseResult.success) { + return { valid: true, data: parseResult.data }; + } else { + return { valid: false, error: parseResult.error.message }; + } + }, + format: (attachment) => { + return { + getRepresentation: () => { + return { type: 'text', value: attachment.data }; + }, + }; + }, + getTools: () => [SECURITY_ENTITY_RISK_SCORE_TOOL_ID], + getAgentDescription: () => { + const description = `You have access to a risk entity that needs to be evaluated. The entity has an identifierType and identifier that you should use to query the risk score. + +RISK ENTITY DATA: +{riskEntityData} + +--- +MANDATORY WORKFLOW: + +1. Extract the identifierType and identifier from the risk entity data above. + +2. Query ENTITY RISK SCORE for the entity: + Tool: ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} + Parameters: { identifierType: "[extracted identifierType]", identifier: "[extracted identifier]" } + +CRITICAL: You MUST call ${sanitizeToolId( + SECURITY_ENTITY_RISK_SCORE_TOOL_ID + )} with the extracted identifierType and identifier before responding.`; + return description; + }, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 258d29a0d2d79..89503d48ede67 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -149,6 +149,7 @@ import type { TelemetryQueryConfiguration } from './lib/telemetry/types'; import { createAlertAttachmentType, createAttackDiscoveryAttachmentType, + createRiskEntityAttachmentType, } from './agent_builder/attachments'; import { alertsTool, @@ -649,6 +650,20 @@ export class Plugin implements ISecuritySolutionPlugin { } } + try { + // Register risk entity attachment type + plugins.onechat.attachments.registerType(createRiskEntityAttachmentType()); + } catch (error) { + if (error instanceof Error && error.message.includes('already registered')) { + this.logger.debug( + 'Risk entity attachment type already registered by onechat plugin, using built-in version' + ); + } else { + this.logger.warn(`Failed to register risk entity attachment type: ${error}`); + // Don't throw - allow plugin to continue loading even if attachment registration fails + } + } + // Register tools try { // plugins.onechat.tools.register(alertsTool()); From 1c2fa8ae025a680b2f0af6f09a76276fb1790f41 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 17:13:19 -0700 Subject: [PATCH 29/96] fix entity risk --- .../tabs/risk_inputs/ask_ai_assistant.tsx | 40 +++++++++++++------ .../flyout/document_details/right/footer.tsx | 3 +- .../agent_builder/attachments/risk_entity.ts | 6 ++- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx index 7a68046866930..de013d05b169a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx @@ -13,9 +13,12 @@ import { getAnonymizedValues } from '@kbn/elastic-assistant-common/impl/data_ano import { getAnonymizedValue } from '@kbn/elastic-assistant-common'; import { useFetchAnonymizationFields } from '@kbn/elastic-assistant'; import type { AnonymizedValues } from '@kbn/elastic-assistant-common/impl/data_anonymization/types'; +import { AttachmentType } from '@kbn/onechat-common/attachments'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { EntityTypeToIdentifierField } from '../../../../../../common/entity_analytics/types'; import type { EntityType } from '../../../../../../common/search_strategy'; +import { NewAgentBuilderAttachment } from '../../../../../agent_builder/components/new_agent_builder_attachment'; +import { useAgentBuilderAttachment } from '../../../../../agent_builder/hooks/use_agent_builder_attachment'; import { useAskAiAssistant } from './use_ask_ai_assistant'; export interface ExplainWithAiAssistantProps { @@ -32,6 +35,7 @@ export const AskAiAssistant = ({ const entityField = EntityTypeToIdentifierField[entityType]; const { data: anonymizationFields } = useFetchAnonymizationFields(); const isAssistantToolDisabled = useIsExperimentalFeatureEnabled('riskScoreAssistantToolDisabled'); + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); const { anonymizedValues, replacements }: AnonymizedValues = useMemo(() => { if (!anonymizationFields.data) { @@ -62,6 +66,12 @@ export const AskAiAssistant = ({ replacements, }); + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ + attachmentType: AttachmentType.risk_entity, + attachmentData: { identifierType: entityType, identifier: entityName }, + attachmentPrompt: `Explain how inputs contributed to the risk score. Additionally, outline the recommended next steps for investigating or mitigating the risk if the entity is deemed risky.\nTo answer risk score questions, fetch the risk score information and take into consideration the risk score inputs.`, + }); + if (aiAssistantDisable || isAssistantToolDisabled) { return null; } @@ -71,19 +81,23 @@ export const AskAiAssistant = ({ - { - showAssistantOverlay(); - }} - > - - + {isAgentBuilderEnabled ? ( + + ) : ( + { + showAssistantOverlay(); + }} + > + + + )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx index 9b1f3525598a7..2f757131481f0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx @@ -10,6 +10,7 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NewChatByTitle } from '@kbn/elastic-assistant'; +import { AttachmentType } from '@kbn/onechat-common/attachments'; import { ALERT_ATTACHMENT_PROMPT } from '../../../agent_builder/components/prompts'; import { useBasicDataFromDetailsData } from '../shared/hooks/use_basic_data_from_details_data'; import { useDocumentDetailsContext } from '../shared/context'; @@ -53,7 +54,7 @@ export const PanelFooter: FC = ({ isRulePreview }) => { }, [dataFormattedForFieldBrowser]); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: 'alert', + attachmentType: AttachmentType.alert, attachmentData: { alert: alertData }, attachmentPrompt: ALERT_ATTACHMENT_PROMPT, }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts index 88b410e348e6a..1347c808abfa2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts @@ -23,7 +23,7 @@ export const createRiskEntityAttachmentType = (): AttachmentTypeDefinition< validate: (input) => { const parseResult = riskEntityAttachmentDataSchema.safeParse(input); if (parseResult.success) { - return { valid: true, data: parseResult.data }; + return { valid: true, data: formatRiskEntityData(parseResult.data) }; } else { return { valid: false, error: parseResult.error.message }; } @@ -58,3 +58,7 @@ CRITICAL: You MUST call ${sanitizeToolId( }, }; }; + +const formatRiskEntityData = (data: RiskEntityAttachmentData): string => { + return `identifier: ${data.identifier}, identifierType: ${data.identifierType}`; +}; From f925a782f35252c16fc55ee7f209508f24c0353f Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 24 Nov 2025 17:18:19 -0700 Subject: [PATCH 30/96] entity risk done --- .../components/entity_highlights_settings.tsx | 75 ++++++++++++++----- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx index 7f39a7db41749..ee51f52af2a47 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx @@ -24,6 +24,10 @@ import { AssistantIcon } from '@kbn/ai-assistant-icon'; import { ConnectorSelectorInline } from '@kbn/elastic-assistant'; import { css } from '@emotion/react'; import { isEmpty } from 'lodash/fp'; +import { AttachmentType } from '@kbn/onechat-common/attachments'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { NewAgentBuilderAttachment } from '../../../../agent_builder/components/new_agent_builder_attachment'; +import { useAgentBuilderAttachment } from '../../../../agent_builder/hooks/use_agent_builder_attachment'; import { useAskAiAssistant } from '../tabs/risk_inputs/use_ask_ai_assistant'; import { getAnonymizedEntityIdentifier } from '../utils/helpers'; @@ -93,6 +97,14 @@ export const EntityHighlightsSettings: React.FC = replacements: assistantResult?.replacements, }); + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); + + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ + attachmentType: AttachmentType.risk_entity, + attachmentData: { identifierType: entityType, identifier: entityIdentifier }, + attachmentPrompt: `Investigate the entity and suggest next steps.`, + }); + const items = useMemo( () => [ @@ -141,26 +153,47 @@ export const EntityHighlightsSettings: React.FC = - { - showAssistantOverlay(); - closePopover(); - }} - icon={} - disabled={isLoading} - > - - + {isAgentBuilderEnabled ? ( + + { + openAgentBuilderFlyout(); + closePopover(); + }} + size="s" + /> + + ) : ( + { + showAssistantOverlay(); + closePopover(); + }} + icon={} + disabled={isLoading} + > + + + )} = connectorId, showAssistantOverlay, closePopover, + isAgentBuilderEnabled, + openAgentBuilderFlyout, ] ); From e5e90c7b7c54bafd8fa2426f03a6442c78e5bb85 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 10:08:23 -0700 Subject: [PATCH 31/96] move attachment definitons --- .../attachments/attachment_types.ts | 45 ---- .../onechat-common/attachments/attachments.ts | 3 - .../onechat-common/attachments/index.ts | 9 - .../security_solution/common/constants.ts | 6 + .../hooks/use_agent_builder_attachment.ts | 5 +- .../hooks/use_agent_builder_attachment.tsx | 244 ------------------ .../actionable_summary/index.tsx | 4 +- .../tabs/attack_discovery_tab/index.tsx | 4 +- .../components/entity_highlights_settings.tsx | 4 +- .../tabs/risk_inputs/ask_ai_assistant.tsx | 4 +- .../flyout/document_details/right/footer.tsx | 4 +- .../server/agent_builder/attachments/alert.ts | 39 ++- .../attachments/attack_discovery.ts | 42 ++- .../agent_builder/attachments/core-alert.ts | 87 ------- .../agent_builder/attachments/risk_entity.ts | 42 ++- .../security_solution/server/plugin.ts | 118 +++++---- 16 files changed, 168 insertions(+), 492 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.tsx delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts index 812143b9c6be7..18e32ca2d028d 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts @@ -16,18 +16,12 @@ export enum AttachmentType { screenContext = 'screen_context', text = 'text', esql = 'esql', - alert = 'alert', - attack_discovery = 'attack_discovery', - risk_entity = 'risk_entity', } interface AttachmentDataMap { [AttachmentType.esql]: EsqlAttachmentData; [AttachmentType.text]: TextAttachmentData; [AttachmentType.screenContext]: ScreenContextAttachmentData; - [AttachmentType.alert]: AlertAttachmentData; - [AttachmentType.attack_discovery]: AttackDiscoveryAttachmentData; - [AttachmentType.risk_entity]: RiskEntityAttachmentData; } export const esqlAttachmentDataSchema = z.object({ @@ -83,43 +77,4 @@ export interface ScreenContextAttachmentData { additional_data?: Record; } -export const alertAttachmentDataSchema = z.object({ - alert: z.string(), -}); - -/** - * Data for an alert attachment. - */ -export interface AlertAttachmentData { - /** The condensed alert data in key-value format (comma-separated, newline-delimited) */ - alert: string; -} - -export const attackDiscoveryAttachmentDataSchema = z.object({ - attackDiscovery: z.string(), -}); - -/** - * Data for an attack discovery attachment. - */ -export interface AttackDiscoveryAttachmentData { - /** The formatted attack discovery data including title, summary, details, and attack chain */ - attackDiscovery: string; -} - -export const riskEntityAttachmentDataSchema = z.object({ - identifierType: z.enum(['host', 'user', 'service', 'generic']), - identifier: z.string().min(1), -}); - -/** - * Data for a risk entity attachment. - */ -export interface RiskEntityAttachmentData { - /** The type of entity: host, user, service, or generic */ - identifierType: 'host' | 'user' | 'service' | 'generic'; - /** The value that identifies the entity (e.g., hostname, username) */ - identifier: string; -} - export type AttachmentDataOf = AttachmentDataMap[Type]; diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts index fcc1e8da01f5d..a63cd31b4b76e 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts @@ -29,9 +29,6 @@ export interface Attachment< export type TextAttachment = Attachment; export type ScreenContextAttachment = Attachment; export type EsqlAttachment = Attachment; -export type AlertAttachment = Attachment; -export type AttackDiscoveryAttachment = Attachment; -export type RiskEntityAttachment = Attachment; /** * Input version of an attachment, where the id is optional diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts index 0aff3ba88779b..efe8c0d98169a 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts @@ -11,22 +11,13 @@ export type { TextAttachment, ScreenContextAttachment, EsqlAttachment, - AlertAttachment, - AttackDiscoveryAttachment, - RiskEntityAttachment, } from './attachments'; export { AttachmentType, textAttachmentDataSchema, esqlAttachmentDataSchema, screenContextAttachmentDataSchema, - alertAttachmentDataSchema, - attackDiscoveryAttachmentDataSchema, - riskEntityAttachmentDataSchema, type TextAttachmentData, type ScreenContextAttachmentData, type EsqlAttachmentData, - type AlertAttachmentData, - type AttackDiscoveryAttachmentData, - type RiskEntityAttachmentData, } from './attachment_types'; diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index c7df2a5660de8..e092897f8f344 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -703,3 +703,9 @@ export const ESSENTIAL_ALERT_FIELDS = [ 'threat.technique.subtechnique.name', 'threat.technique.subtechnique.reference', ] as const; + +export enum SecurityAgentBuilderAttachments { + attack_discovery = 'attack_discovery', + alert = 'alert', + risk_entity = 'alert', +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts index e278db33acd1e..6d0bc86c6dda3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { useCallback, useState } from 'react'; -import React from 'react'; +import React, { useCallback, useState } from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '@kbn/react-kibana-mount'; @@ -162,7 +161,7 @@ export const useAgentBuilderAttachment = ({ ? toMountPoint( React.createElement(AgentBuilderToastSuccessContent, { content: renderContent, - onViewConversationClick: onViewConversationClick, + onViewConversationClick, }), { i18n: i18nService, theme, userProfile } ) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.tsx deleted file mode 100644 index c2367c6c8df27..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.tsx +++ /dev/null @@ -1,244 +0,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 Elastic License - * 2.0. - */ - -import React, { useCallback, useState, useRef, useEffect } from 'react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '@kbn/react-kibana-mount'; -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiText, - logicalCSS, - useEuiTheme, -} from '@elastic/eui'; -import { css } from '@emotion/react'; -import { useIsMounted } from '@kbn/securitysolution-hook-utils'; -import { useAppToasts } from '../../common/hooks/use_app_toasts'; -import { useAppUrl } from '../../common/lib/kibana/hooks'; - -export interface UseAgentBuilderAttachmentParams { - /** - * Type of attachment (e.g., 'alert', 'attack_discovery') - */ - attachmentType: string; - /** - * Data for the attachment - */ - attachmentData: Record; - /** - * Prompt/input text for the agent builder conversation - */ - attachmentPrompt: string; -} - -export interface UseAgentBuilderAttachmentResult { - /** - * Function to open the agent builder flyout - * TODO: This currently calls the API directly as a temporary implementation. - * Once the agent builder UI is ready, this will open a flyout with the attachment data instead. - */ - openAgentBuilderFlyout: () => void; - /** - * Whether the API call is in progress - */ - isLoading: boolean; -} - -/** - * Hook to handle agent builder attachment functionality. - * Temporarily calls the API directly until the agent builder UI is ready. - * Eventually, this will open a flyout with the attachment data. - * - * Improvements over previous implementation: - * - Uses useIsMounted to prevent state updates after component unmounts - * - Implements abort controller for request cancellation - * - Prevents race conditions by cancelling previous requests when a new one starts - * - Proper cleanup on unmount - */ -export const useAgentBuilderAttachment = ({ - attachmentType, - attachmentData, - attachmentPrompt, -}: UseAgentBuilderAttachmentParams): UseAgentBuilderAttachmentResult => { - const { http, application, i18n: i18nService, theme, userProfile } = useKibana().services; - const toasts = useAppToasts(); - const { getAppUrl } = useAppUrl(); - const [isLoading, setIsLoading] = useState(false); - const isMounted = useIsMounted(); - const abortControllerRef = useRef(null); - - // Cleanup abort controller on unmount - useEffect(() => { - return () => { - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - abortControllerRef.current = null; - } - }; - }, []); - - const openAgentBuilderFlyout = useCallback(async () => { - // Cancel any previous request - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - - // Create new abort controller for this request - const abortController = new AbortController(); - abortControllerRef.current = abortController; - - // Only update loading state if component is still mounted - if (!isMounted()) { - return; - } - setIsLoading(true); - - try { - // TODO: This API call is temporary until the agent builder UI is ready. - // Once the UI is ready, this will open a flyout with the attachment data instead. - const result = await http.post('/api/agent_builder/converse', { - body: JSON.stringify({ - input: attachmentPrompt, - attachments: [ - { - type: attachmentType, - data: attachmentData, - }, - ], - }), - version: '2023-10-31', - signal: abortController.signal, - }); - - // Check if request was aborted or component unmounted - if (abortController.signal.aborted || !isMounted()) { - return; - } - - const conversationId = result?.conversation_id; - const conversationUrl = conversationId - ? getAppUrl({ - appId: 'agent_builder', - path: `/conversations/${conversationId}`, - }) - : null; - - const onViewConversationClick = () => { - if (conversationUrl) { - application.navigateToUrl(conversationUrl); - } - }; - - const renderContent = i18n.translate( - 'xpack.securitySolution.agentBuilder.attachment.successText', - { - defaultMessage: 'Your attachment has been sent to the agent builder.', - } - ); - - toasts.addSuccess({ - color: 'success', - iconType: 'check', - title: i18n.translate('xpack.securitySolution.agentBuilder.attachment.successTitle', { - defaultMessage: 'Agent builder conversation started', - }), - text: conversationUrl - ? toMountPoint( - , - { i18n: i18nService, theme, userProfile } - ) - : renderContent, - }); - } catch (error) { - // Don't show error toast if request was aborted or component unmounted - if (abortController.signal.aborted || !isMounted()) { - return; - } - - toasts.addError(error, { - title: i18n.translate('xpack.securitySolution.agentBuilder.attachment.errorTitle', { - defaultMessage: 'Failed to start agent builder conversation', - }), - toastMessage: i18n.translate('xpack.securitySolution.agentBuilder.attachment.errorText', { - defaultMessage: 'There was an error sending your attachment to the agent builder.', - }), - }); - } finally { - // Only update loading state if this request wasn't aborted and component is still mounted - if (!abortController.signal.aborted && isMounted()) { - setIsLoading(false); - } - - // Clear the abort controller ref if this was the current request - if (abortControllerRef.current === abortController) { - abortControllerRef.current = null; - } - } - }, [ - attachmentType, - attachmentData, - attachmentPrompt, - http, - toasts, - getAppUrl, - application, - i18nService, - theme, - userProfile, - isMounted, - ]); - - return { - openAgentBuilderFlyout, - isLoading, - }; -}; - -const AgentBuilderToastSuccessContent: React.FC<{ - onViewConversationClick?: () => void; - content?: string; -}> = ({ onViewConversationClick, content }) => { - const { euiTheme } = useEuiTheme(); - return ( - <> - {content !== undefined ? ( - - {content} - - ) : null} - {onViewConversationClick !== undefined ? ( - - - - {i18n.translate( - 'xpack.securitySolution.agentBuilder.attachment.viewConversationButton', - { - defaultMessage: 'View conversation', - } - )} - - - - ) : null} - - ); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx index 06315feced1bf..f9c09eef1133f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx @@ -12,9 +12,9 @@ import { replaceAnonymizedValuesWithOriginalValues, type Replacements, } from '@kbn/elastic-assistant-common'; -import { AttachmentType } from '@kbn/onechat-common/attachments'; import React, { useMemo } from 'react'; +import { SecurityAgentBuilderAttachments } from '../../../../../../common/constants'; import { ATTACK_DISCOVERY_ATTACHMENT_PROMPT } from '../../../../../agent_builder/components/prompts'; import { SECURITY_FEATURE_ID } from '../../../../../../common'; import { useKibana } from '../../../../../common/lib/kibana'; @@ -86,7 +86,7 @@ const ActionableSummaryComponent: React.FC = ({ ); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: AttachmentType.attack_discovery, + attachmentType: SecurityAgentBuilderAttachments.attack_discovery, attachmentData: { attackDiscovery: attackDiscoveryWithOriginalValues }, attachmentPrompt: ATTACK_DISCOVERY_ATTACHMENT_PROMPT, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx index 648d0764eadbc..429f586ab2a08 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx @@ -10,11 +10,11 @@ import { getAttackDiscoveryMarkdown, replaceAnonymizedValuesWithOriginalValues, } from '@kbn/elastic-assistant-common'; -import { AttachmentType } from '@kbn/onechat-common/attachments'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiTitle, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import React, { useMemo } from 'react'; +import { SecurityAgentBuilderAttachments } from '../../../../../../../common/constants'; import { ATTACK_DISCOVERY_ATTACHMENT_PROMPT } from '../../../../../../agent_builder/components/prompts'; import { useKibana } from '../../../../../../common/lib/kibana'; import { AttackChain } from './attack/attack_chain'; @@ -97,7 +97,7 @@ const AttackDiscoveryTabComponent: React.FC = ({ ); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: AttachmentType.attack_discovery, + attachmentType: SecurityAgentBuilderAttachments.attack_discovery, attachmentData: { attackDiscovery: attackDiscoveryWithOriginalValues }, attachmentPrompt: ATTACK_DISCOVERY_ATTACHMENT_PROMPT, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx index ee51f52af2a47..cbac00bc956cc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx @@ -24,12 +24,12 @@ import { AssistantIcon } from '@kbn/ai-assistant-icon'; import { ConnectorSelectorInline } from '@kbn/elastic-assistant'; import { css } from '@emotion/react'; import { isEmpty } from 'lodash/fp'; -import { AttachmentType } from '@kbn/onechat-common/attachments'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { NewAgentBuilderAttachment } from '../../../../agent_builder/components/new_agent_builder_attachment'; import { useAgentBuilderAttachment } from '../../../../agent_builder/hooks/use_agent_builder_attachment'; import { useAskAiAssistant } from '../tabs/risk_inputs/use_ask_ai_assistant'; import { getAnonymizedEntityIdentifier } from '../utils/helpers'; +import { SecurityAgentBuilderAttachments } from '../../../../../common/constants'; interface EntityHighlightsSettingsProps { onRegenerate: () => void; @@ -100,7 +100,7 @@ export const EntityHighlightsSettings: React.FC = const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: AttachmentType.risk_entity, + attachmentType: SecurityAgentBuilderAttachments.risk_entity, attachmentData: { identifierType: entityType, identifier: entityIdentifier }, attachmentPrompt: `Investigate the entity and suggest next steps.`, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx index de013d05b169a..8c6102cb7fb4f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx @@ -13,13 +13,13 @@ import { getAnonymizedValues } from '@kbn/elastic-assistant-common/impl/data_ano import { getAnonymizedValue } from '@kbn/elastic-assistant-common'; import { useFetchAnonymizationFields } from '@kbn/elastic-assistant'; import type { AnonymizedValues } from '@kbn/elastic-assistant-common/impl/data_anonymization/types'; -import { AttachmentType } from '@kbn/onechat-common/attachments'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { EntityTypeToIdentifierField } from '../../../../../../common/entity_analytics/types'; import type { EntityType } from '../../../../../../common/search_strategy'; import { NewAgentBuilderAttachment } from '../../../../../agent_builder/components/new_agent_builder_attachment'; import { useAgentBuilderAttachment } from '../../../../../agent_builder/hooks/use_agent_builder_attachment'; import { useAskAiAssistant } from './use_ask_ai_assistant'; +import { SecurityAgentBuilderAttachments } from '../../../../../../common/constants'; export interface ExplainWithAiAssistantProps { entityType: T; @@ -67,7 +67,7 @@ export const AskAiAssistant = ({ }); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: AttachmentType.risk_entity, + attachmentType: SecurityAgentBuilderAttachments.risk_entity, attachmentData: { identifierType: entityType, identifier: entityName }, attachmentPrompt: `Explain how inputs contributed to the risk score. Additionally, outline the recommended next steps for investigating or mitigating the risk if the entity is deemed risky.\nTo answer risk score questions, fetch the risk score information and take into consideration the risk score inputs.`, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx index 2f757131481f0..81949736470a0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx @@ -10,7 +10,6 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NewChatByTitle } from '@kbn/elastic-assistant'; -import { AttachmentType } from '@kbn/onechat-common/attachments'; import { ALERT_ATTACHMENT_PROMPT } from '../../../agent_builder/components/prompts'; import { useBasicDataFromDetailsData } from '../shared/hooks/use_basic_data_from_details_data'; import { useDocumentDetailsContext } from '../shared/context'; @@ -21,6 +20,7 @@ import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_exper import { NewAgentBuilderAttachment } from '../../../agent_builder/components/new_agent_builder_attachment'; import { useAgentBuilderAttachment } from '../../../agent_builder/hooks/use_agent_builder_attachment'; import { getRawData, filterAndStringifyAlertData } from '../../../assistant/helpers'; +import { SecurityAgentBuilderAttachments } from '../../../../common/constants'; export const ASK_AI_ASSISTANT = i18n.translate( 'xpack.securitySolution.ease.flyout.right.footer.askAIAssistant', @@ -54,7 +54,7 @@ export const PanelFooter: FC = ({ isRulePreview }) => { }, [dataFormattedForFieldBrowser]); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: AttachmentType.alert, + attachmentType: SecurityAgentBuilderAttachments.alert, attachmentData: { alert: alertData }, attachmentPrompt: ALERT_ATTACHMENT_PROMPT, }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index a2fd57b318ba0..2adf55af9855c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -5,27 +5,42 @@ * 2.0. */ -import type { AlertAttachmentData } from '@kbn/onechat-common/attachments'; -import { AttachmentType, alertAttachmentDataSchema } from '@kbn/onechat-common/attachments'; +import { z } from '@kbn/zod'; import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import type { Attachment } from '@kbn/onechat-common/attachments'; import { platformCoreTools } from '@kbn/onechat-common'; +import { SecurityAgentBuilderAttachments } from '../../../common/constants'; import { SECURITY_ENTITY_RISK_SCORE_TOOL_ID, SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, SECURITY_LABS_SEARCH_TOOL_ID, } from '../tools'; +export const alertAttachmentDataSchema = z.object({ + alert: z.string(), +}); + +/** + * Data for an alert attachment. + */ +export type AlertAttachmentData = z.infer; + +/** + * Type guard to narrow attachment data to AlertAttachmentData + */ +const isAlertAttachmentData = (data: unknown): data is AlertAttachmentData => { + return alertAttachmentDataSchema.safeParse(data).success; +}; + /** * Creates the definition for the `alert` attachment type. */ -export const createAlertAttachmentType = (): AttachmentTypeDefinition< - AttachmentType.alert, - AlertAttachmentData -> => { +export const createAlertAttachmentType = (): AttachmentTypeDefinition => { return { - id: AttachmentType.alert, + id: SecurityAgentBuilderAttachments.alert, validate: (input) => { + console.log('alert validate'); const parseResult = alertAttachmentDataSchema.safeParse(input); if (parseResult.success) { return { valid: true, data: parseResult.data }; @@ -33,10 +48,16 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition< return { valid: false, error: parseResult.error.message }; } }, - format: (attachment) => { + format: (attachment: Attachment) => { + // Extract data to allow proper type narrowing + const data = attachment.data; + // Type narrowing: validation ensures data matches AlertAttachmentData + if (!isAlertAttachmentData(data)) { + throw new Error(`Invalid alert attachment data for attachment ${attachment.id}`); + } return { getRepresentation: () => { - return { type: 'text', value: formatAlertData(attachment.data) }; + return { type: 'text', value: formatAlertData(data) }; }, }; }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts index 879d86c6f4e46..e2842cb398572 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts @@ -5,26 +5,38 @@ * 2.0. */ -import type { AttackDiscoveryAttachmentData } from '@kbn/onechat-common/attachments'; -import { - AttachmentType, - attackDiscoveryAttachmentDataSchema, -} from '@kbn/onechat-common/attachments'; +import { z } from '@kbn/zod'; import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import type { Attachment } from '@kbn/onechat-common/attachments'; import { platformCoreTools } from '@kbn/onechat-common'; +import { SecurityAgentBuilderAttachments } from '../../../common/constants'; import { SECURITY_ENTITY_RISK_SCORE_TOOL_ID, SECURITY_LABS_SEARCH_TOOL_ID } from '../tools'; +export const attackDiscoveryAttachmentDataSchema = z.object({ + attackDiscovery: z.string(), +}); + +/** + * Data for an attack discovery attachment. + */ +export type AttackDiscoveryAttachmentData = z.infer; + +/** + * Type guard to narrow attachment data to AttackDiscoveryAttachmentData + */ +const isAttackDiscoveryAttachmentData = (data: unknown): data is AttackDiscoveryAttachmentData => { + return attackDiscoveryAttachmentDataSchema.safeParse(data).success; +}; + /** * Creates the definition for the `attack_discovery` attachment type. */ -export const createAttackDiscoveryAttachmentType = (): AttachmentTypeDefinition< - AttachmentType.attack_discovery, - AttackDiscoveryAttachmentData -> => { +export const createAttackDiscoveryAttachmentType = (): AttachmentTypeDefinition => { return { - id: AttachmentType.attack_discovery, + id: SecurityAgentBuilderAttachments.attack_discovery, validate: (input) => { + console.log('attack_discovery validate'); const parseResult = attackDiscoveryAttachmentDataSchema.safeParse(input); if (parseResult.success) { return { valid: true, data: parseResult.data }; @@ -32,10 +44,16 @@ export const createAttackDiscoveryAttachmentType = (): AttachmentTypeDefinition< return { valid: false, error: parseResult.error.message }; } }, - format: (attachment) => { + format: (attachment: Attachment) => { + // Extract data to allow proper type narrowing + const data = attachment.data; + // Type narrowing: validation ensures data matches AttackDiscoveryAttachmentData + if (!isAttackDiscoveryAttachmentData(data)) { + throw new Error(`Invalid attack discovery attachment data for attachment ${attachment.id}`); + } return { getRepresentation: () => { - return { type: 'text', value: formatAttackDiscoveryData(attachment.data) }; + return { type: 'text', value: formatAttackDiscoveryData(data) }; }, }; }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts deleted file mode 100644 index 7364f85316fb5..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/core-alert.ts +++ /dev/null @@ -1,87 +0,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 Elastic License - * 2.0. - */ - -import type { AlertAttachmentData } from '@kbn/onechat-common/attachments'; -import { AttachmentType, alertAttachmentDataSchema } from '@kbn/onechat-common/attachments'; -import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; -import { platformCoreTools } from '@kbn/onechat-common/tools'; - -/** - * Creates the definition for the `alert` attachment type. - */ -export const createAlertAttachmentType = (): AttachmentTypeDefinition< - AttachmentType.alert, - AlertAttachmentData -> => { - return { - id: AttachmentType.alert, - validate: (input) => { - const parseResult = alertAttachmentDataSchema.safeParse(input); - if (parseResult.success) { - return { valid: true, data: parseResult.data }; - } else { - return { valid: false, error: parseResult.error.message }; - } - }, - format: (attachment) => { - return { - getRepresentation: () => { - return { type: 'text', value: formatAlertData(attachment.data) }; - }, - }; - }, - getTools: () => { - const tools = [ - platformCoreTools.generateEsql, - platformCoreTools.executeEsql, - platformCoreTools.search, - ]; - return tools; - }, - getAgentDescription: () => { - const description = `You have access to security alert data. To provide a comprehensive analysis, you MUST gather enriched context by querying for related information. - -SECURITY ALERT DATA: -{alertData} - ---- - -MANDATORY WORKFLOW - Complete in order: - -1. Extract entities: host.name, user.name, source.ip, destination.ip, file.hash.sha256, kibana.alert.uuid (or _id), kibana.alert.rule.name, kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, event.category, event.action - -2. Query RELATED ALERTS: - Tool: ${platformCoreTools.search} - Parameters: { query: "Find security alerts from last 7 days where host.name is '[host]' OR user.name is '[user]' OR source.ip is '[ip]' OR destination.ip is '[dest_ip]'", index: ".alerts-security.alerts-default" } - -3. Query RISK SCORES: - Tool: ${platformCoreTools.search} - Parameters: { query: "Find risk scores for host.name '[host]' OR user.name '[user]'", index: "risk-score.risk-score-latest-default" } - -4. Query ATTACK DISCOVERIES: - Tool: ${platformCoreTools.search} - Parameters: { query: "Find attack discoveries where kibana.alert.attack_discovery.alert_ids contains '[alert ID]'", index: ".alerts-security.alerts-attack.discovery-default,.adhoc.alerts-security.alerts-attack.discovery-default" } - -5. Query SECURITY LABS: - Tool: ${platformCoreTools.search} - Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]", index: ".kibana-elastic-ai-assistant-knowledge-base-default" } - -CRITICAL: You MUST call all 4 tools (steps 2-5) before responding. Do not skip any step.`; - return description; - }, - }; -}; - -/** - * Formats alert data for display. - * - * @param data - The alert attachment data containing the alert string - * @returns Formatted string representation of the alert data - */ -const formatAlertData = (data: AlertAttachmentData): string => { - return data.alert; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts index 1347c808abfa2..fcd393c75a8c3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts @@ -4,22 +4,38 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import type { RiskEntityAttachmentData } from '@kbn/onechat-common/attachments'; -import { AttachmentType, riskEntityAttachmentDataSchema } from '@kbn/onechat-common/attachments'; +import { z } from '@kbn/zod'; import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import type { Attachment } from '@kbn/onechat-common/attachments'; +import { SecurityAgentBuilderAttachments } from '../../../common/constants'; import { SECURITY_ENTITY_RISK_SCORE_TOOL_ID } from '../tools'; +const riskEntityAttachmentDataSchema = z.object({ + identifierType: z.enum(['host', 'user', 'service', 'generic']), + identifier: z.string().min(1), +}); + +/** + * Data for a risk entity attachment. + * Note: After validation, the data is stored as a formatted string. + */ +type RiskEntityAttachmentData = z.infer; + +/** + * Type guard to check if data is a formatted risk entity string + */ +const isRiskEntityFormattedData = (data: unknown): data is string => { + return ( + typeof data === 'string' && data.includes('identifier:') && data.includes('identifierType:') + ); +}; /** * Creates the definition for the `risk_entity` attachment type. */ -export const createRiskEntityAttachmentType = (): AttachmentTypeDefinition< - AttachmentType.risk_entity, - RiskEntityAttachmentData -> => { +export const createRiskEntityAttachmentType = (): AttachmentTypeDefinition => { return { - id: AttachmentType.risk_entity, + id: SecurityAgentBuilderAttachments.alert, validate: (input) => { const parseResult = riskEntityAttachmentDataSchema.safeParse(input); if (parseResult.success) { @@ -28,10 +44,16 @@ export const createRiskEntityAttachmentType = (): AttachmentTypeDefinition< return { valid: false, error: parseResult.error.message }; } }, - format: (attachment) => { + format: (attachment: Attachment) => { + // Extract data to allow proper type narrowing + const data = attachment.data; + // Type narrowing: validation ensures data is a formatted string + if (!isRiskEntityFormattedData(data)) { + throw new Error(`Invalid risk entity attachment data for attachment ${attachment.id}`); + } return { getRepresentation: () => { - return { type: 'text', value: attachment.data }; + return { type: 'text', value: data }; }, }; }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 89503d48ede67..a204279fd5c7d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -152,9 +152,6 @@ import { createRiskEntityAttachmentType, } from './agent_builder/attachments'; import { - alertsTool, - alertsIndexSearchTool, - evaluateAlertTool, entityRiskScoreTool, attackDiscoverySearchTool, securityLabsSearchTool, @@ -238,6 +235,63 @@ export class Plugin implements ISecuritySolutionPlugin { this.healthDiagnosticService = new HealthDiagnosticServiceImpl(this.logger); } + private registerOnechatAttachmentsAndTools( + onechat: SecuritySolutionPluginSetupDependencies['onechat'], + config: ConfigType + ): void { + if (!onechat || !config.experimentalFeatures.agentBuilderEnabled) { + return; + } + + // Register alert attachment type + try { + onechat.attachments.registerType(createAlertAttachmentType()); + } catch (error) { + if (error instanceof Error && error.message.includes('already registered')) { + this.logger.debug( + 'Alert attachment type already registered by onechat plugin, using built-in version' + ); + } else { + this.logger.warn(`Failed to register alert attachment type: ${error}`); + } + } + + // Register attack discovery attachment type + try { + onechat.attachments.registerType(createAttackDiscoveryAttachmentType()); + } catch (error) { + if (error instanceof Error && error.message.includes('already registered')) { + this.logger.debug( + 'Attack discovery attachment type already registered by onechat plugin, using built-in version' + ); + } else { + this.logger.warn(`Failed to register attack discovery attachment type: ${error}`); + } + } + + // Register risk entity attachment type + try { + onechat.attachments.registerType(createRiskEntityAttachmentType()); + } catch (error) { + if (error instanceof Error && error.message.includes('already registered')) { + this.logger.debug( + 'Risk entity attachment type already registered by onechat plugin, using built-in version' + ); + } else { + this.logger.warn(`Failed to register risk entity attachment type: ${error}`); + } + } + + // Register tools + try { + onechat.tools.register(entityRiskScoreTool()); + onechat.tools.register(attackDiscoverySearchTool()); + onechat.tools.register(securityLabsSearchTool()); + } catch (error) { + this.logger.warn(`Failed to register onechat tools: ${error}`); + } + } + public setup( core: SecuritySolutionPluginCoreSetupDependencies, plugins: SecuritySolutionPluginSetupDependencies @@ -620,63 +674,7 @@ export class Plugin implements ISecuritySolutionPlugin { // Note: This requires onechat to be added as an optional plugin dependency // Note: The alert attachment type may already be registered by onechat's built-in types. // If so, we'll skip registration and use the built-in version. - if (plugins.onechat && config.experimentalFeatures.agentBuilderEnabled) { - try { - // Register attachment type - plugins.onechat.attachments.registerType(createAlertAttachmentType()); - } catch (error) { - // Alert attachment type may already be registered by onechat's built-in types - if (error instanceof Error && error.message.includes('already registered')) { - this.logger.debug( - 'Alert attachment type already registered by onechat plugin, using built-in version' - ); - } else { - this.logger.warn(`Failed to register alert attachment type: ${error}`); - // Don't throw - allow plugin to continue loading even if attachment registration fails - } - } - - try { - // Register attack discovery attachment type - plugins.onechat.attachments.registerType(createAttackDiscoveryAttachmentType()); - } catch (error) { - if (error instanceof Error && error.message.includes('already registered')) { - this.logger.debug( - 'Attack discovery attachment type already registered by onechat plugin, using built-in version' - ); - } else { - this.logger.warn(`Failed to register attack discovery attachment type: ${error}`); - // Don't throw - allow plugin to continue loading even if attachment registration fails - } - } - - try { - // Register risk entity attachment type - plugins.onechat.attachments.registerType(createRiskEntityAttachmentType()); - } catch (error) { - if (error instanceof Error && error.message.includes('already registered')) { - this.logger.debug( - 'Risk entity attachment type already registered by onechat plugin, using built-in version' - ); - } else { - this.logger.warn(`Failed to register risk entity attachment type: ${error}`); - // Don't throw - allow plugin to continue loading even if attachment registration fails - } - } - - // Register tools - try { - // plugins.onechat.tools.register(alertsTool()); - // plugins.onechat.tools.register(alertsIndexSearchTool()); - // plugins.onechat.tools.register(evaluateAlertTool()); - plugins.onechat.tools.register(entityRiskScoreTool()); - plugins.onechat.tools.register(attackDiscoverySearchTool()); - plugins.onechat.tools.register(securityLabsSearchTool()); - } catch (error) { - this.logger.warn(`Failed to register onechat tools: ${error}`); - // Don't throw - allow plugin to continue loading even if tool registration fails - } - } + this.registerOnechatAttachmentsAndTools(plugins.onechat, config); return { setProductFeaturesConfigurator: From 3441fc90911278f69afeae599f9ffae6de02a410 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 10:09:15 -0700 Subject: [PATCH 32/96] fix whitespace --- .../onechat/server/services/agents/modes/default/prompts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/prompts.ts b/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/prompts.ts index e0915c84f7543..58e329680bbab 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/prompts.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/prompts.ts @@ -320,6 +320,7 @@ const renderAttachmentTypeInstructions = (attachmentTypes: ProcessedAttachmentTy const perTypeInstructions = attachmentTypes.map(({ type, description }) => { return `### ${type} attachments + ${description ?? 'No instructions available.'} `; }); From 2786c24608928b680457a6daa7c9242e552ac119 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 10:24:26 -0700 Subject: [PATCH 33/96] fixing --- .../attachments/attachment_type_registry.ts | 3 ++ .../security_solution/common/constants.ts | 6 +-- .../hooks/use_agent_builder_attachment.ts | 28 ++++++++----- .../server/agent_builder/attachments/alert.ts | 1 - .../attachments/attack_discovery.ts | 1 - .../agent_builder/attachments/risk_entity.ts | 2 +- .../security_solution/server/plugin.ts | 41 ++----------------- 7 files changed, 27 insertions(+), 55 deletions(-) diff --git a/x-pack/platform/plugins/shared/onechat/server/services/attachments/attachment_type_registry.ts b/x-pack/platform/plugins/shared/onechat/server/services/attachments/attachment_type_registry.ts index 943a2ab9d0c69..1b2057971ade2 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/attachments/attachment_type_registry.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/attachments/attachment_type_registry.ts @@ -24,9 +24,12 @@ class AttachmentTypeRegistryImpl implements AttachmentTypeRegistry { constructor() {} register(type: AttachmentTypeDefinition) { + console.log('register ==>', type); if (this.attachmentTypes.has(type.id)) { + console.log('register error ==>', type.id); throw new Error(`Attachment type with id ${type.id} already registered`); } + console.log('register add ==>', type.id); this.attachmentTypes.set(type.id, type); } diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index e092897f8f344..96c416881675e 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -705,7 +705,7 @@ export const ESSENTIAL_ALERT_FIELDS = [ ] as const; export enum SecurityAgentBuilderAttachments { - attack_discovery = 'attack_discovery', - alert = 'alert', - risk_entity = 'alert', + attack_discovery = 'security.attack_discovery', + alert = 'security.alert', + risk_entity = 'security.risk_entity', } diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts index 6d0bc86c6dda3..d93fef0c919f8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts @@ -18,6 +18,7 @@ import { useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/react'; +import type { ChatResponse } from '@kbn/onechat-plugin/common/http_api/chat'; import { useAppToasts } from '../../common/hooks/use_app_toasts'; import { useAppUrl } from '../../common/lib/kibana/hooks'; @@ -113,11 +114,15 @@ export const useAgentBuilderAttachment = ({ const [isLoading, setIsLoading] = useState(false); const openAgentBuilderFlyout = useCallback(async () => { + if (!http || !application) { + return; + } + setIsLoading(true); try { // TODO: This API call is temporary until the agent builder UI is ready. // Once the UI is ready, this will open a flyout with the attachment data instead. - const result = await http.post('/api/agent_builder/converse', { + const result = await http.post('/api/agent_builder/converse', { body: JSON.stringify({ input: attachmentPrompt, attachments: [ @@ -139,7 +144,7 @@ export const useAgentBuilderAttachment = ({ : null; const onViewConversationClick = () => { - if (conversationUrl) { + if (conversationUrl && application) { application.navigateToUrl(conversationUrl); } }; @@ -157,15 +162,16 @@ export const useAgentBuilderAttachment = ({ title: i18n.translate('xpack.securitySolution.agentBuilder.attachment.successTitle', { defaultMessage: 'Agent builder conversation started', }), - text: conversationUrl - ? toMountPoint( - React.createElement(AgentBuilderToastSuccessContent, { - content: renderContent, - onViewConversationClick, - }), - { i18n: i18nService, theme, userProfile } - ) - : renderContent, + text: + conversationUrl && i18nService && theme + ? toMountPoint( + React.createElement(AgentBuilderToastSuccessContent, { + content: renderContent, + onViewConversationClick, + }), + { i18n: i18nService, theme, userProfile } + ) + : renderContent, }); } catch (error) { toasts.addError(error, { diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 2adf55af9855c..533db8fa53803 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -40,7 +40,6 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition => { return { id: SecurityAgentBuilderAttachments.alert, validate: (input) => { - console.log('alert validate'); const parseResult = alertAttachmentDataSchema.safeParse(input); if (parseResult.success) { return { valid: true, data: parseResult.data }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts index e2842cb398572..a76ce4df4aa27 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts @@ -36,7 +36,6 @@ export const createAttackDiscoveryAttachmentType = (): AttachmentTypeDefinition return { id: SecurityAgentBuilderAttachments.attack_discovery, validate: (input) => { - console.log('attack_discovery validate'); const parseResult = attackDiscoveryAttachmentDataSchema.safeParse(input); if (parseResult.success) { return { valid: true, data: parseResult.data }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts index fcd393c75a8c3..34fb2bcb31bf1 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts @@ -35,7 +35,7 @@ const isRiskEntityFormattedData = (data: unknown): data is string => { */ export const createRiskEntityAttachmentType = (): AttachmentTypeDefinition => { return { - id: SecurityAgentBuilderAttachments.alert, + id: SecurityAgentBuilderAttachments.risk_entity, validate: (input) => { const parseResult = riskEntityAttachmentDataSchema.safeParse(input); if (parseResult.success) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index a204279fd5c7d..2936cbc87f330 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -243,44 +243,9 @@ export class Plugin implements ISecuritySolutionPlugin { return; } - // Register alert attachment type - try { - onechat.attachments.registerType(createAlertAttachmentType()); - } catch (error) { - if (error instanceof Error && error.message.includes('already registered')) { - this.logger.debug( - 'Alert attachment type already registered by onechat plugin, using built-in version' - ); - } else { - this.logger.warn(`Failed to register alert attachment type: ${error}`); - } - } - - // Register attack discovery attachment type - try { - onechat.attachments.registerType(createAttackDiscoveryAttachmentType()); - } catch (error) { - if (error instanceof Error && error.message.includes('already registered')) { - this.logger.debug( - 'Attack discovery attachment type already registered by onechat plugin, using built-in version' - ); - } else { - this.logger.warn(`Failed to register attack discovery attachment type: ${error}`); - } - } - - // Register risk entity attachment type - try { - onechat.attachments.registerType(createRiskEntityAttachmentType()); - } catch (error) { - if (error instanceof Error && error.message.includes('already registered')) { - this.logger.debug( - 'Risk entity attachment type already registered by onechat plugin, using built-in version' - ); - } else { - this.logger.warn(`Failed to register risk entity attachment type: ${error}`); - } - } + onechat.attachments.registerType(createAlertAttachmentType()); + onechat.attachments.registerType(createAttackDiscoveryAttachmentType()); + onechat.attachments.registerType(createRiskEntityAttachmentType()); // Register tools try { From 34f4f1fcd16f4726f419eefad18aff06477234d1 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 10:36:29 -0700 Subject: [PATCH 34/96] rm logs --- .../server/services/attachments/attachment_type_registry.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/platform/plugins/shared/onechat/server/services/attachments/attachment_type_registry.ts b/x-pack/platform/plugins/shared/onechat/server/services/attachments/attachment_type_registry.ts index 1b2057971ade2..943a2ab9d0c69 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/attachments/attachment_type_registry.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/attachments/attachment_type_registry.ts @@ -24,12 +24,9 @@ class AttachmentTypeRegistryImpl implements AttachmentTypeRegistry { constructor() {} register(type: AttachmentTypeDefinition) { - console.log('register ==>', type); if (this.attachmentTypes.has(type.id)) { - console.log('register error ==>', type.id); throw new Error(`Attachment type with id ${type.id} already registered`); } - console.log('register add ==>', type.id); this.attachmentTypes.set(type.id, type); } From 8738c0e646cff49c03dd73373af50d41c722dbca Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 12:17:20 -0700 Subject: [PATCH 35/96] add product reference attachment --- .../attachments/attachment_types.ts | 14 ++++ .../onechat-common/attachments/attachments.ts | 1 + .../onechat-common/attachments/index.ts | 3 + .../services/attachments/definitions/index.ts | 2 + .../definitions/product_reference.ts | 73 +++++++++++++++++++ 5 files changed, 93 insertions(+) create mode 100644 x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/product_reference.ts diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts index 18e32ca2d028d..ae0850719905a 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts @@ -16,12 +16,14 @@ export enum AttachmentType { screenContext = 'screen_context', text = 'text', esql = 'esql', + product_reference = 'product_reference', } interface AttachmentDataMap { [AttachmentType.esql]: EsqlAttachmentData; [AttachmentType.text]: TextAttachmentData; [AttachmentType.screenContext]: ScreenContextAttachmentData; + [AttachmentType.product_reference]: ProductReferenceAttachmentData; } export const esqlAttachmentDataSchema = z.object({ @@ -77,4 +79,16 @@ export interface ScreenContextAttachmentData { additional_data?: Record; } +export const productReferenceAttachmentDataSchema = z.object({ + text: z.string(), +}); + +/** + * Data for a product reference attachment. + */ +export interface ProductReferenceAttachmentData { + /** Text content describing the product reference or query */ + text: string; +} + export type AttachmentDataOf = AttachmentDataMap[Type]; diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts index a63cd31b4b76e..c1f4c2e62bdb2 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts @@ -29,6 +29,7 @@ export interface Attachment< export type TextAttachment = Attachment; export type ScreenContextAttachment = Attachment; export type EsqlAttachment = Attachment; +export type ProductReferenceAttachment = Attachment; /** * Input version of an attachment, where the id is optional diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts index efe8c0d98169a..d50e9a4abfa2b 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts @@ -11,13 +11,16 @@ export type { TextAttachment, ScreenContextAttachment, EsqlAttachment, + ProductReferenceAttachment, } from './attachments'; export { AttachmentType, textAttachmentDataSchema, esqlAttachmentDataSchema, screenContextAttachmentDataSchema, + productReferenceAttachmentDataSchema, type TextAttachmentData, type ScreenContextAttachmentData, type EsqlAttachmentData, + type ProductReferenceAttachmentData, } from './attachment_types'; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts index 4591a7e2d9666..0aa7b4d809575 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/index.ts @@ -10,12 +10,14 @@ import type { AttachmentTypeRegistry } from '../attachment_type_registry'; import { createTextAttachmentType } from './text'; import { createEsqlAttachmentType } from './esql'; import { createScreenContextAttachmentType } from './screen_context'; +import { createProductReferenceAttachmentType } from './product_reference'; export const registerAttachmentTypes = ({ registry }: { registry: AttachmentTypeRegistry }) => { const attachmentTypes: AttachmentTypeDefinition[] = [ createTextAttachmentType(), createScreenContextAttachmentType(), createEsqlAttachmentType(), + createProductReferenceAttachmentType(), ]; attachmentTypes.forEach((attachmentType) => { diff --git a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/product_reference.ts b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/product_reference.ts new file mode 100644 index 0000000000000..d668d74b4de74 --- /dev/null +++ b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/product_reference.ts @@ -0,0 +1,73 @@ +/* + * 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. + */ + +import type { ProductReferenceAttachmentData } from '@kbn/onechat-common/attachments'; +import { + AttachmentType, + productReferenceAttachmentDataSchema, +} from '@kbn/onechat-common/attachments'; +import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; + +/** + * Creates the definition for the `product_reference` attachment type. + */ +export const createProductReferenceAttachmentType = (): AttachmentTypeDefinition< + AttachmentType.product_reference, + ProductReferenceAttachmentData +> => { + return { + id: AttachmentType.product_reference, + validate: (input) => { + const parseResult = productReferenceAttachmentDataSchema.safeParse(input); + if (parseResult.success) { + return { valid: true, data: parseResult.data }; + } else { + return { valid: false, error: parseResult.error.message }; + } + }, + format: (attachment) => { + return { + getRepresentation: () => { + return { type: 'text', value: formatProductReferenceData(attachment.data) }; + }, + }; + }, + // TODO use real tool once https://github.com/elastic/kibana/pull/242598 merges, same in description below + getTools: () => [`platformCoreTools.productDocumentation`], + getAgentDescription: () => { + const description = `You have access to a product reference that needs to be queried for documentation. + +PRODUCT REFERENCE DATA: +{productReferenceData} + +--- +MANDATORY WORKFLOW: + +1. Extract the query or topic from the product reference data above. + +2. Query PRODUCT DOCUMENTATION for relevant documentation: + Tool: ${sanitizeToolId(`platformCoreTools.productDocumentation`)} + Parameters: { + query: "[extracted query or topic from the product reference]", + product: "[optional: 'kibana' | 'elasticsearch' | 'observability' | 'security']", + max: 3 + }`; + return description; + }, + }; +}; + +/** + * Formats product reference data for display. + * + * @param data - The product reference attachment data containing the text + * @returns Formatted string representation of the product reference data + */ +const formatProductReferenceData = (data: ProductReferenceAttachmentData): string => { + return data.text; +}; From 42c3fc2385c19cf2f23a0036c88fee090fed981e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 12:34:20 -0700 Subject: [PATCH 36/96] rules agent step --- .../new_agent_builder_attachment.tsx | 22 +++++--- .../rule_status_failed_callout.tsx | 54 ++++++++++++++----- .../rule_execution_status/translations.ts | 7 +++ .../rules_table/rules_table_toolbar.tsx | 39 ++++++++++---- 4 files changed, 93 insertions(+), 29 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx index 89bcc680529d1..024d6b6e5eb3c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx @@ -5,22 +5,22 @@ * 2.0. */ -import type { EuiButtonColor } from '@elastic/eui'; +import type { EuiButtonColor, IconType } from '@elastic/eui'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import React from 'react'; import type { EuiButtonEmptySizes } from '@elastic/eui/src/components/button/button_empty/button_empty'; import * as i18n from './translations'; -export const BUTTON_TEST_ID = 'newAgentBuilderAttachment'; -export const BUTTON_ICON_TEST_ID = 'newAgentBuilderAttachmentIcon'; -export const BUTTON_TEXT_TEST_ID = 'newAgentBuilderAttachmentText'; - export interface NewAgentBuilderAttachmentProps { /** * Optionally specify color of empty button. * @default 'primary' */ color?: EuiButtonColor; + /** + * icon type + */ + iconType?: IconType; /** * Callback when button is clicked */ @@ -29,16 +29,22 @@ export interface NewAgentBuilderAttachmentProps { * Size of the button */ size?: EuiButtonEmptySizes; + /** + * Optional button text + */ + text?: string; } const NewAgentBuilderAttachmentComponent: React.FC = ({ color = 'primary', + iconType = 'machineLearningApp', onClick, size = 'm', + text = i18n.VIEW_IN_AGENT_BUILDER, }) => { return ( - + - {i18n.VIEW_IN_AGENT_BUILDER} + {text} ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.tsx index db3e73b337e3d..1cdc352558b32 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.tsx @@ -11,12 +11,16 @@ import { css } from '@emotion/react'; import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; import { NewChat } from '@kbn/elastic-assistant'; +import { AttachmentType } from '@kbn/onechat-common/attachments'; import { FormattedDate } from '../../../../common/components/formatted_date'; import type { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring'; import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring'; import * as i18n from './translations'; import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { NewAgentBuilderAttachment } from '../../../../agent_builder/components/new_agent_builder_attachment'; +import { useAgentBuilderAttachment } from '../../../../agent_builder/hooks/use_agent_builder_attachment'; interface RuleStatusFailedCallOutProps { ruleNameForChat: string; @@ -49,6 +53,22 @@ const RuleStatusFailedCallOutComponent: React.FC = return `${ruleNameForChat} - ${title} ${date}`; }, [date, title, ruleNameForChat]); + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); + const attachmentData = useMemo( + () => ({ + text: + ruleName != null && dataSources != null + ? `Rule name: ${ruleName}\nData sources: ${dataSources}\nError message: ${message}` + : `Error message: ${message}`, + }), + [message, ruleName, dataSources] + ); + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ + attachmentType: AttachmentType.product_reference, + attachmentData, + attachmentPrompt: i18n.ASK_ASSISTANT_USER_PROMPT, + }); + if (!shouldBeDisplayed) { return null; } @@ -83,18 +103,28 @@ const RuleStatusFailedCallOutComponent: React.FC = {message} {hasAssistantPrivilege && ( - - {i18n.ASK_ASSISTANT_ERROR_BUTTON} - + <> + {isAgentBuilderEnabled ? ( + + ) : ( + + {i18n.ASK_ASSISTANT_ERROR_BUTTON} + + )} + )}
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/translations.ts index 3b760ac68e628..178fa5afe46da 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/translations.ts @@ -56,6 +56,13 @@ export const ASK_ASSISTANT_ERROR_BUTTON = i18n.translate( } ); +export const ASK_AGENT_ERROR_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleStatus.askAgent', + { + defaultMessage: 'Ask Agent', + } +); + export const ASK_ASSISTANT_DESCRIPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleStatus.askAssistantDesc', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx index 0e2fa2c3f1c06..d9ba46a64d938 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { NewChat } from '@kbn/elastic-assistant'; +import { AttachmentType } from '@kbn/onechat-common/attachments'; import { useUserData } from '../../../../detections/components/user_info'; import { TabNavigation } from '../../../../common/components/navigation/tab_navigation'; import { usePrebuiltRulesStatus } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_status'; @@ -17,6 +18,9 @@ import { getPromptContextFromDetectionRules } from '../../../../assistant/helper import { useRulesTableContext } from './rules_table/rules_table_context'; import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; import * as i18nAssistant from '../../../common/translations'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { NewAgentBuilderAttachment } from '../../../../agent_builder/components/new_agent_builder_attachment'; +import { useAgentBuilderAttachment } from '../../../../agent_builder/hooks/use_agent_builder_attachment'; export enum AllRulesTabs { management = 'management', @@ -97,6 +101,17 @@ export const RulesTableToolbar = React.memo(() => { return `${i18nAssistant.DETECTION_RULES_CONVERSATION_ID} - ${selectedRuleNames.join(', ')}`; }, [selectedRuleNames]); + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); + const attachmentData = useMemo( + () => ({ text: getPromptContextFromDetectionRules(selectedRules) }), + [selectedRules] + ); + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ + attachmentType: AttachmentType.product_reference, + attachmentData, + attachmentPrompt: i18nAssistant.EXPLAIN_THEN_SUMMARIZE_RULE_DETAILS, + }); + return ( @@ -104,15 +119,21 @@ export const RulesTableToolbar = React.memo(() => { {hasAssistantPrivilege && selectedRules.length > 0 && isAssistantEnabled && ( - + <> + {isAgentBuilderEnabled ? ( + + ) : ( + + )} + )} From 141fbcc58b2d27b421b0c42e9940b4f8138d30b1 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 13:05:04 -0700 Subject: [PATCH 37/96] pre rule creation attachment --- .../pages/results/take_action/index.tsx | 59 +++++++++++++++---- .../pages/results/take_action/translations.ts | 7 +++ .../detection_engine/common/translations.ts | 7 +++ .../components/ai_assistant/index.tsx | 25 +++++++- .../components/ai_assistant/translations.ts | 6 ++ .../rules_table/rules_table_toolbar.tsx | 6 +- 6 files changed, 96 insertions(+), 14 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx index 5aaf07e952c5a..846216abc124d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx @@ -21,6 +21,7 @@ import { } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import { SecurityAgentBuilderAttachments } from '../../../../../common/constants'; import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; import { useAddToNewCase } from './use_add_to_case'; import { useAddToExistingCase } from './use_add_to_existing_case'; @@ -32,6 +33,8 @@ import { UpdateAlertsModal } from './update_alerts_modal'; import { useAttackDiscoveryBulk } from '../../use_attack_discovery_bulk'; import { useUpdateAlertsStatus } from './use_update_alerts_status'; import { isAttackDiscoveryAlert } from '../../utils/is_attack_discovery_alert'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { useAgentBuilderAttachment } from '../../../../agent_builder/hooks/use_agent_builder_attachment'; interface Props { attackDiscoveries: AttackDiscovery[] | AttackDiscoveryAlert[]; @@ -205,6 +208,26 @@ const TakeActionComponent: React.FC = ({ showAssistantOverlay?.(); }, [closePopover, showAssistantOverlay]); + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); + const attackDiscovery = attackDiscoveries.length === 1 ? attackDiscoveries[0] : null; + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ + attachmentType: SecurityAgentBuilderAttachments.attack_discovery, + attachmentData: { + attackDiscovery: attackDiscovery + ? getAttackDiscoveryMarkdown({ + attackDiscovery, + replacements, + }) + : '', + }, + attachmentPrompt: i18n.VIEW_IN_AGENT_BUILDER, + }); + + const onViewInAgentBuilder = useCallback(() => { + closePopover(); + openAgentBuilderFlyout(); + }, [closePopover, openAgentBuilderFlyout]); + // button for the popover: const button = useMemo( () => ( @@ -243,24 +266,36 @@ const TakeActionComponent: React.FC = ({ {i18n.ADD_TO_EXISTING_CASE} , - attackDiscoveries.length === 1 ? ( - - {i18n.VIEW_IN_AI_ASSISTANT} - - ) : ( - [] - ), + attackDiscoveries.length === 1 + ? isAgentBuilderEnabled + ? [ + + {i18n.VIEW_IN_AGENT_BUILDER} + , + ] + : [ + + {i18n.VIEW_IN_AI_ASSISTANT} + , + ] + : [], ].flat(), [ addToCaseDisabled, attackDiscoveries.length, + isAgentBuilderEnabled, onClickAddToExistingCase, onClickAddToNewCase, + onViewInAgentBuilder, onViewInAiAssistant, viewInAiAssistantDisabled, ] diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/translations.ts index e1b742db5ea17..075539018f084 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/translations.ts @@ -55,3 +55,10 @@ export const VIEW_IN_AI_ASSISTANT = i18n.translate( defaultMessage: 'View in AI Assistant', } ); + +export const VIEW_IN_AGENT_BUILDER = i18n.translate( + 'xpack.securitySolution.attackDiscovery.attackDiscoveryPanel.actions.takeAction.viewInAgentBuilderButtonLabel', + { + defaultMessage: 'View in Agent Builder', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/translations.ts index 1c6105e9016cd..6327fb92a7b8f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/translations.ts @@ -1628,3 +1628,10 @@ export const COLUMN_TOTAL_UNFILLED_GAPS_DURATION_TOOLTIP = i18n.translate( defaultMessage: 'Sum of remaining unfilled or partially filled gaps', } ); + +export const CHAT_IN_AGENT_BUILDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.chatInAgentBuilder', + { + defaultMessage: 'Chat in Agent Builder', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx index 4dbab1d9a8043..81a4866489dba 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx @@ -10,6 +10,7 @@ import { EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { NewChat } from '@kbn/elastic-assistant'; +import { AttachmentType } from '@kbn/onechat-common/attachments'; import { AssistantIcon } from '@kbn/ai-assistant-icon'; import { css } from '@emotion/react'; @@ -17,6 +18,9 @@ import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../../common/lib/tele import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; import type { DefineStepRule } from '../../../common/types'; import type { FormHook, ValidationError } from '../../../../shared_imports'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { NewAgentBuilderAttachment } from '../../../../agent_builder/components/new_agent_builder_attachment'; +import { useAgentBuilderAttachment } from '../../../../agent_builder/hooks/use_agent_builder_attachment'; import * as i18n from './translations'; @@ -96,6 +100,19 @@ Proposed solution should be valid and must not contain new line symbols (\\n)`; return `${i18n.DETECTION_RULES_CREATE_FORM_CONVERSATION_ID} - ${query ?? 'query'}`; }, [getFields]); + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); + const attachmentData = useMemo(() => { + const queryField = getFields().queryBar; + const { query } = (queryField.value as DefineStepRule['queryBar']).query; + return { query: query ?? '' }; + }, [getFields]); + + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ + attachmentType: AttachmentType.esql, + attachmentData, + attachmentPrompt: i18n.ASK_ASSISTANT_USER_PROMPT(languageName), + }); + if (!hasAssistantPrivilege) { return null; } @@ -108,7 +125,13 @@ Proposed solution should be valid and must not contain new line symbols (\\n)`; id="xpack.securitySolution.detectionEngine.createRule.stepDefineRule.askAssistantHelpText" defaultMessage="{AiAssistantNewChatLink} to help resolve this error." values={{ - AiAssistantNewChatLink: ( + AiAssistantNewChatLink: isAgentBuilderEnabled ? ( + + ) : ( { {hasAssistantPrivilege && selectedRules.length > 0 && isAssistantEnabled && ( <> {isAgentBuilderEnabled ? ( - + ) : ( Date: Tue, 25 Nov 2025 13:39:35 -0700 Subject: [PATCH 38/96] coreSecurity => security --- .../packages/shared/onechat/onechat-common/base/namespaces.ts | 4 ++-- .../security_solution/server/agent_builder/tools/constants.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/base/namespaces.ts b/x-pack/platform/packages/shared/onechat/onechat-common/base/namespaces.ts index a364591e6d53f..dc514d9c55690 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/base/namespaces.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/base/namespaces.ts @@ -12,7 +12,7 @@ export const internalNamespaces = { platformCore: 'platform.core', observability: 'observability', - coreSecurity: 'core.security', + security: 'security', } as const; /** @@ -21,7 +21,7 @@ export const internalNamespaces = { export const protectedNamespaces: string[] = [ internalNamespaces.platformCore, internalNamespaces.observability, - internalNamespaces.coreSecurity, + internalNamespaces.security, ]; /** diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts index b4645048ad03b..72d40ad500ccc 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/constants.ts @@ -11,5 +11,5 @@ import { internalNamespaces } from '@kbn/onechat-common/base/namespaces'; * Creates a security tool ID with the core.security namespace. */ export const securityTool = (toolName: string): string => { - return `${internalNamespaces.coreSecurity}.${toolName}`; + return `${internalNamespaces.security}.${toolName}`; }; From 7d10732315d31db029de67a59a7f8ac3ce05ec7b Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 13:57:00 -0700 Subject: [PATCH 39/96] query help added --- .../security_solution/common/constants.ts | 3 +- .../public/assistant/helpers.tsx | 11 +-- .../server/agent_builder/attachments/alert.ts | 3 +- .../attachments/attack_discovery.ts | 4 +- .../server/agent_builder/attachments/index.ts | 1 + .../agent_builder/attachments/query_help.ts | 99 +++++++++++++++++++ .../agent_builder/attachments/risk_entity.ts | 3 +- .../security_solution/server/plugin.ts | 2 + 8 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index 96c416881675e..d3ef14d6dceb0 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -566,7 +566,7 @@ export const PROMOTION_RULE_TAGS = [ * Essential fields to return for security alerts to reduce context window usage. * These fields contain the most relevant information for security analysis. */ -export const ESSENTIAL_ALERT_FIELDS = [ +export const ESSENTIAL_ALERT_FIELDS: string[] = [ '_id', '@timestamp', 'message', @@ -708,4 +708,5 @@ export enum SecurityAgentBuilderAttachments { attack_discovery = 'security.attack_discovery', alert = 'security.alert', risk_entity = 'security.risk_entity', + query_help = 'security.query_help', } diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx index 5637397f503a8..c0f1747e094ea 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx @@ -28,13 +28,12 @@ export const getRawData = (data: TimelineEventsDetailsItem[]): Record): string => { - const essentialFieldsSet = new Set(ESSENTIAL_ALERT_FIELDS); - const filteredData = Object.keys(rawData) - .filter((key) => essentialFieldsSet.has(key)) - .reduce((acc, key) => { + const filteredData = ESSENTIAL_ALERT_FIELDS.reduce((acc, key) => { + if (key in rawData) { acc[key] = rawData[key]; - return acc; - }, {} as Record); + } + return acc; + }, {} as Record); return JSON.stringify(filteredData); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 533db8fa53803..774bbcdb24ef6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -50,7 +50,8 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition => { format: (attachment: Attachment) => { // Extract data to allow proper type narrowing const data = attachment.data; - // Type narrowing: validation ensures data matches AlertAttachmentData + // Necessary because we cannot currently use the AttachmentType type as agent is not + // registered with enum AttachmentType in onechat attachment_types.ts if (!isAlertAttachmentData(data)) { throw new Error(`Invalid alert attachment data for attachment ${attachment.id}`); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts index a76ce4df4aa27..224fb9a727950 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts @@ -46,7 +46,9 @@ export const createAttackDiscoveryAttachmentType = (): AttachmentTypeDefinition format: (attachment: Attachment) => { // Extract data to allow proper type narrowing const data = attachment.data; - // Type narrowing: validation ensures data matches AttackDiscoveryAttachmentData + + // Necessary because we cannot currently use AttachmentType type as agent is not + // registered with enum AttachmentType in onechat attachment_types.ts if (!isAttackDiscoveryAttachmentData(data)) { throw new Error(`Invalid attack discovery attachment data for attachment ${attachment.id}`); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts index 90097a81be92e..4bccfd85feb1c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts @@ -8,4 +8,5 @@ export { createAlertAttachmentType } from './alert'; export { createAttackDiscoveryAttachmentType } from './attack_discovery'; export { createRiskEntityAttachmentType } from './risk_entity'; +export { createQueryHelpAttachmentType } from './query_help'; // export { createAlertAttachmentType } from './core-alert'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts new file mode 100644 index 0000000000000..0e84ae525002b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts @@ -0,0 +1,99 @@ +/* + * 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. + */ + +import { z } from '@kbn/zod'; +import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; +import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import type { Attachment } from '@kbn/onechat-common/attachments'; +import { platformCoreTools } from '@kbn/onechat-common'; +import { SecurityAgentBuilderAttachments } from '../../../common/constants'; + +export const queryHelpAttachmentDataSchema = z.object({ + query: z.string(), + queryLanguage: z.string(), +}); + +export type QueryHelpAttachmentData = z.infer; + +const isQueryHelpAttachmentData = (data: unknown): data is QueryHelpAttachmentData => { + return queryHelpAttachmentDataSchema.safeParse(data).success; +}; + +export const createQueryHelpAttachmentType = (): AttachmentTypeDefinition => { + return { + id: SecurityAgentBuilderAttachments.query_help, + validate: (input) => { + const parseResult = queryHelpAttachmentDataSchema.safeParse(input); + if (parseResult.success) { + return { valid: true, data: parseResult.data }; + } else { + return { valid: false, error: parseResult.error.message }; + } + }, + format: (attachment: Attachment) => { + // Extract data to allow proper type narrowing + const data = attachment.data; + // Necessary because we cannot currently use the AttachmentType type as agent is not + // registered with enum AttachmentType in onechat attachment_types.ts + if (!isQueryHelpAttachmentData(data)) { + throw new Error(`Invalid query help attachment data for attachment ${attachment.id}`); + } + return { + getRepresentation: () => { + return { type: 'text', value: formatQueryHelpData(data) }; + }, + }; + }, + getTools: () => { + const tools: string[] = [ + // TODO use real tool once product_documentation tool is merged, same in description below + 'platformCoreTools.productDocumentation', + ]; + // Note: generateEsql is conditionally available based on queryLanguage + // We include it in the tools list, but the agent should only use it when queryLanguage is 'esql' + tools.push(platformCoreTools.generateEsql); + return tools; + }, + getAgentDescription: () => { + const description = `The following is a broken query: {query}. Generate a new working query using the generateEsql tool (only if queryLanguage is 'esql') and productDocumentationTool when appropriate. + +QUERY HELP DATA: +{queryHelpData} + +--- +MANDATORY WORKFLOW: + +1. Check the queryLanguage from the query help data above. + +2. If queryLanguage is 'esql', use the generateEsql tool to generate a new working ESQL query: + Tool: ${sanitizeToolId(platformCoreTools.generateEsql)} + Parameters: { query: "Write ESQL query to [describe what the broken query was trying to do]" } + +3. Query PRODUCT DOCUMENTATION for relevant documentation when needed: + // TODO use real tool once product_documentation tool is merged + Tool: ${sanitizeToolId('platformCoreTools.productDocumentation')} + Parameters: { + query: "[query about ESQL syntax, query language, or related documentation]", + product: "[optional: 'kibana' | 'elasticsearch' | 'observability' | 'security']", + max: 3 + } + +CRITICAL: Only use the generateEsql tool if queryLanguage is 'esql'. Otherwise, use productDocumentationTool to find relevant documentation to help fix the query.`; + return description; + }, + }; +}; + +/** + * Formats query help data for display. + * + * @param data - The query help attachment data containing the query and queryLanguage + * @returns Formatted string representation of the query help data + */ +const formatQueryHelpData = (data: QueryHelpAttachmentData): string => { + return `Query: ${data.query}\nQuery Language: ${data.queryLanguage}`; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts index 34fb2bcb31bf1..a0b5a9136b1bc 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts @@ -47,7 +47,8 @@ export const createRiskEntityAttachmentType = (): AttachmentTypeDefinition => { format: (attachment: Attachment) => { // Extract data to allow proper type narrowing const data = attachment.data; - // Type narrowing: validation ensures data is a formatted string + // Necessary because we cannot currently use the AttachmentType type as agent is not + // registered with enum AttachmentType in onechat attachment_types.ts if (!isRiskEntityFormattedData(data)) { throw new Error(`Invalid risk entity attachment data for attachment ${attachment.id}`); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 2936cbc87f330..e74be53c81b6c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -150,6 +150,7 @@ import { createAlertAttachmentType, createAttackDiscoveryAttachmentType, createRiskEntityAttachmentType, + createQueryHelpAttachmentType, } from './agent_builder/attachments'; import { entityRiskScoreTool, @@ -246,6 +247,7 @@ export class Plugin implements ISecuritySolutionPlugin { onechat.attachments.registerType(createAlertAttachmentType()); onechat.attachments.registerType(createAttackDiscoveryAttachmentType()); onechat.attachments.registerType(createRiskEntityAttachmentType()); + onechat.attachments.registerType(createQueryHelpAttachmentType()); // Register tools try { From 70257fcf8d5ce8150565ec3794e953806e9dab06 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 14:25:31 -0700 Subject: [PATCH 40/96] use query help --- .../rule_creation_ui/components/ai_assistant/index.tsx | 8 ++++---- .../server/agent_builder/attachments/query_help.ts | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx index 81a4866489dba..7f90c753a3381 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx @@ -10,10 +10,10 @@ import { EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { NewChat } from '@kbn/elastic-assistant'; -import { AttachmentType } from '@kbn/onechat-common/attachments'; import { AssistantIcon } from '@kbn/ai-assistant-icon'; import { css } from '@emotion/react'; +import { SecurityAgentBuilderAttachments } from '../../../../../common/constants'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../../common/lib/telemetry'; import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; import type { DefineStepRule } from '../../../common/types'; @@ -104,11 +104,11 @@ Proposed solution should be valid and must not contain new line symbols (\\n)`; const attachmentData = useMemo(() => { const queryField = getFields().queryBar; const { query } = (queryField.value as DefineStepRule['queryBar']).query; - return { query: query ?? '' }; - }, [getFields]); + return { query: query ?? '', queryLanguage: language }; + }, [getFields, language]); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: AttachmentType.esql, + attachmentType: SecurityAgentBuilderAttachments.query_help, attachmentData, attachmentPrompt: i18n.ASK_ASSISTANT_USER_PROMPT(languageName), }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts index 0e84ae525002b..70cb88ff71274 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts @@ -50,12 +50,10 @@ export const createQueryHelpAttachmentType = (): AttachmentTypeDefinition => { }, getTools: () => { const tools: string[] = [ + platformCoreTools.generateEsql, // TODO use real tool once product_documentation tool is merged, same in description below 'platformCoreTools.productDocumentation', ]; - // Note: generateEsql is conditionally available based on queryLanguage - // We include it in the tools list, but the agent should only use it when queryLanguage is 'esql' - tools.push(platformCoreTools.generateEsql); return tools; }, getAgentDescription: () => { @@ -77,7 +75,7 @@ MANDATORY WORKFLOW: // TODO use real tool once product_documentation tool is merged Tool: ${sanitizeToolId('platformCoreTools.productDocumentation')} Parameters: { - query: "[query about ESQL syntax, query language, or related documentation]", + query: "[query about KQL query language, Elasticsearch Query DSL, Lucene]", product: "[optional: 'kibana' | 'elasticsearch' | 'observability' | 'security']", max: 3 } From 3124311edbec4e0cc0f6b7d596c0a742a32a0177 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 14:45:49 -0700 Subject: [PATCH 41/96] generic entity --- .../agent_builder/components/prompts.ts | 15 +++++++++- .../entity_details/generic_right/footer.tsx | 30 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts index 60456b01d91ac..04da2828fce08 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +// temporary until we figure out if we are using prompt system or not export const ATTACK_DISCOVERY_ATTACHMENT_PROMPT = `Summarize the attack discovery attached and recommend next steps. Case URLs MUST be included in the response if they exist. Summary should be in markdown.`; export const ALERT_ATTACHMENT_PROMPT = `Evaluate the provided security alert and generate a structured, markdown-formatted summary suitable for inclusion in an Elastic Security case. Use all available enrichment tools before generating your response. Include the following sections: @@ -78,3 +78,16 @@ export const ALERT_ATTACHMENT_PROMPT = `Evaluate the provided security alert and * Include emojis in section headers for clarity **CRITICAL:** You MUST incorporate results from **all enrichment tools** (risk scores, attack discoveries, related cases, Security Labs) before generating the response. Do not skip any step.`; +export const ENTITY_ANALYSIS = `Analyze asset data described above to provide security insights. The data contains the context of a specific asset (e.g., a host, user, service or cloud resource). Your response must be structured, contextual, and provide a general analysis based on the structure below. +Your response must be in markdown format and include the following sections: +**1. 🔍 Asset Overview** + - Begin by acknowledging the asset you are analyzing using its primary identifiers (e.g., "Analyzing host \`[host.name]\` with IP \`[host.ip]\`"). + - Provide a concise summary of the asset's most critical attributes from the provided context. + - Describe its key relationships and dependencies (e.g., "This asset is part of the \`[cloud.project.name]\` project and is located in the \`[cloud.availability_zone]\` zone."). +**2. 💡 Investigation & Analytics** + - Based on the asset's type and attributes, suggest potential investigation paths or common attack vectors. + - **Generate one contextual ES|QL query** to help the user investigate further. Your generated query should address a common analytical question related to the asset type and sub type. Suggest other possible queries and ask if the user wants to generate more queries. +**General Instructions:** +- **Context Awareness:** Your entire analysis must be derived from the provided asset context. If a piece of information is not available in the context state that and proceed with the available data. +- **Query Generation:** When generating a query, your primary output for that section should be a valid, ready-to-use ES|QL query based on the asset's schema. Use ES|QL tool for query generation. Format all queries as code blocks. +- **Formatting:** Use markdown headers, tables, code blocks, and bullet points to ensure the output is clear, organized, and easily readable. Use concise, actionable language.`; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx index 8558ae89f7f9f..f00c01e82010b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx @@ -11,6 +11,7 @@ import type { EntityEcs } from '@kbn/securitysolution-ecs/src/entity'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { i18n } from '@kbn/i18n'; import { NewChatByTitle } from '@kbn/elastic-assistant'; +import { AttachmentType } from '@kbn/onechat-common/attachments'; import { DocumentEventTypes } from '../../../common/lib/telemetry'; import { TakeAction } from '../shared/components/take_action'; import { @@ -23,6 +24,10 @@ import { useKibana } from '../../../common/lib/kibana'; import { ASK_AI_ASSISTANT } from '../shared/translations'; import { useAssetInventoryAssistant } from './hooks/use_asset_inventory_assistant'; import type { AssetCriticalityLevel } from '../../../../common/api/entity_analytics/asset_criticality'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import { NewAgentBuilderAttachment } from '../../../agent_builder/components/new_agent_builder_attachment'; +import { useAgentBuilderAttachment } from '../../../agent_builder/hooks/use_agent_builder_attachment'; +import { ENTITY_ANALYSIS } from '../../../agent_builder/components/prompts'; interface GenericEntityFlyoutFooterProps { entityId: EntityEcs['id']; @@ -49,6 +54,22 @@ export const GenericEntityFlyoutFooter = ({ assetCriticalityLevel, }); + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); + + const attachmentData = useMemo(() => { + return { + ...entityFields, + 'asset.criticality': assetCriticalityLevel ? [assetCriticalityLevel] : undefined, + }; + }, [entityFields, assetCriticalityLevel]); + + // TODO confirm behavior with @maxcold https://github.com/elastic/kibana/pull/234324 + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ + attachmentType: AttachmentType.text, + attachmentData, + attachmentPrompt: ENTITY_ANALYSIS, + }); + const openDocumentFlyout = useCallback(() => { openFlyout({ right: { @@ -89,7 +110,14 @@ export const GenericEntityFlyoutFooter = ({ {showAssistant && ( - + {isAgentBuilderEnabled ? ( + + ) : ( + + )} )} From c1b915e1478ef8d321fda56db7d1ca06a04bf922 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 15:08:36 -0700 Subject: [PATCH 42/96] EASE --- .../public/flyout/ease/footer.tsx | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx index 9f7125de0dac4..dbccfa709adc1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter, EuiPanel } from '@elastic/eui'; import { NewChatByTitle } from '@kbn/elastic-assistant'; import { i18n } from '@kbn/i18n'; @@ -13,6 +13,12 @@ import { TakeActionButton } from './components/take_action_button'; import { useEaseDetailsContext } from './context'; import { useBasicDataFromDetailsData } from '../document_details/shared/hooks/use_basic_data_from_details_data'; import { useAssistant } from '../document_details/right/hooks/use_assistant'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; +import { NewAgentBuilderAttachment } from '../../agent_builder/components/new_agent_builder_attachment'; +import { useAgentBuilderAttachment } from '../../agent_builder/hooks/use_agent_builder_attachment'; +import { getRawData, filterAndStringifyAlertData } from '../../assistant/helpers'; +import { SecurityAgentBuilderAttachments } from '../../../common/constants'; +import { ALERT_ATTACHMENT_PROMPT } from '../../agent_builder/components/prompts'; export const ASK_AI_ASSISTANT = i18n.translate( 'xpack.securitySolution.flyout.right.footer.askAIAssistant', @@ -33,6 +39,18 @@ export const PanelFooter = memo(() => { dataFormattedForFieldBrowser, isAlert, }); + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); + + const alertData = useMemo(() => { + const rawData = getRawData(dataFormattedForFieldBrowser ?? []); + return filterAndStringifyAlertData(rawData); + }, [dataFormattedForFieldBrowser]); + // This will not work until we add permissions to EASE roles for read_onechat + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ + attachmentType: SecurityAgentBuilderAttachments.alert, + attachmentData: { alert: alertData }, + attachmentPrompt: ALERT_ATTACHMENT_PROMPT, + }); return ( @@ -40,7 +58,14 @@ export const PanelFooter = memo(() => { {showAssistant && ( - + {isAgentBuilderEnabled ? ( + + ) : ( + + )} )} From 1929c9d9d52fa0be88e7f3954c9b147e9997218f Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 15:16:09 -0700 Subject: [PATCH 43/96] cleanup --- .../onechat/onechat-server/allow_lists.ts | 10 +- .../tools/alerts/alerts_index_search_tool.ts | 119 ----- .../agent_builder/tools/alerts/alerts_tool.ts | 127 ----- .../tools/alerts/evaluate_alert_tool.ts | 494 ------------------ .../agent_builder/tools/alerts/helpers.ts | 26 - .../tools/attack_discovery_search_tool.ts | 8 +- .../tools/security_labs_search_tool.ts | 4 +- 7 files changed, 10 insertions(+), 778 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/helpers.ts diff --git a/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts b/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts index 81243d3a3479d..b9318009b7e98 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-server/allow_lists.ts @@ -6,6 +6,7 @@ */ import { platformCoreTools } from '@kbn/onechat-common/tools'; +import { internalNamespaces } from '@kbn/onechat-common/base/namespaces'; /** * This is a manually maintained list of all built-in tools registered in Agent Builder. @@ -22,12 +23,9 @@ export const AGENT_BUILDER_BUILTIN_TOOLS: string[] = [ 'observability.get_services', 'observability.get_downstream_dependencies', // Security Solution - // 'core.security.alerts', - // 'core.security.alerts_index_search', - // 'core.security.evaluate_alert', - 'core.security.entity_risk_score', - 'core.security.attack_discovery_search', - 'core.security.security_labs_search', + `${internalNamespaces.security}.entity_risk_score`, + `${internalNamespaces.security}.attack_discovery_search`, + `${internalNamespaces.security}.security_labs_search`, ]; /** diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts deleted file mode 100644 index e5e04361c7bce..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_index_search_tool.ts +++ /dev/null @@ -1,119 +0,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 Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod'; -import { ToolType } from '@kbn/onechat-common'; -import { generateEsql } from '@kbn/onechat-genai-utils/tools'; -import type { BuiltinToolDefinition } from '@kbn/onechat-server'; -import { DEFAULT_ALERTS_INDEX, ESSENTIAL_ALERT_FIELDS } from '../../../../common/constants'; -import { getSpaceIdFromRequest } from '../helpers'; -import { securityTool } from '../constants'; - -const alertsIndexSearchSchema = z.object({ - query: z - .string() - .describe( - 'A natural language query expressing the search request for security alerts. Use this to find related alerts by entities like host.name, user.name, source.ip, destination.ip, file.hash.sha256, etc.' - ), -}); - -export const SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID = securityTool('alerts_index_search'); - -const KEEP_FIELDS = ESSENTIAL_ALERT_FIELDS.join(', '); - -const ADDITIONAL_INSTRUCTIONS = `When querying security alert indices, ALWAYS use the KEEP command to filter fields and reduce response size. Include these essential fields: ${KEEP_FIELDS}. Limit results to 50 alerts. Example: FROM .alerts-security.alerts-* | KEEP ${KEEP_FIELDS} | ...`; - -export const alertsIndexSearchTool = (): BuiltinToolDefinition => { - return { - id: SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID, - type: ToolType.builtin, - description: `Search and analyze security alerts from the alerts index. Use this tool to find related alerts by entities like host names, user names, IP addresses, file hashes, or other alert fields. Automatically filters to essential fields and limits results to 50 alerts.`, - schema: alertsIndexSearchSchema, - handler: async ({ query: nlQuery }, { request, esClient, modelProvider, logger, events }) => { - const spaceId = getSpaceIdFromRequest(request); - const searchIndex = `${DEFAULT_ALERTS_INDEX}-${spaceId}`; - - logger.debug( - `${SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID} tool called with query: ${nlQuery}, index: ${searchIndex}` - ); - - try { - // Generate ES|QL query with automatic KEEP field filtering - const esqlResponse = await generateEsql({ - nlQuery, - index: searchIndex, - additionalInstructions: ADDITIONAL_INSTRUCTIONS, - executeQuery: true, - model: await modelProvider.getDefaultModel(), - esClient: esClient.asCurrentUser, - logger, - events, - }); - - if (esqlResponse.error) { - return { - results: [ - { - type: 'error', - data: { - message: esqlResponse.error, - }, - }, - ], - }; - } - - const results = []; - - if (esqlResponse.query) { - results.push({ - type: 'query', - data: { - esql: esqlResponse.query, - }, - }); - } - - if (esqlResponse.results) { - results.push({ - type: 'tabularData', - data: { - source: 'esql', - query: esqlResponse.query || '', - columns: esqlResponse.results.columns, - values: esqlResponse.results.values, - }, - }); - } - - if (esqlResponse.answer) { - results.push({ - type: 'other', - data: { - answer: esqlResponse.answer, - }, - }); - } - - return { results }; - } catch (error) { - logger.error(`Error in ${SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID} tool: ${error.message}`); - return { - results: [ - { - type: 'error', - data: { - message: `Error: ${error.message}`, - }, - }, - ], - }; - } - }, - tags: ['security', 'alerts', 'search'], - }; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts deleted file mode 100644 index f6a4797a5050c..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/alerts_tool.ts +++ /dev/null @@ -1,127 +0,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 Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod'; -import { ToolType } from '@kbn/onechat-common'; -import { generateEsql } from '@kbn/onechat-genai-utils/tools'; -import type { BuiltinToolDefinition } from '@kbn/onechat-server'; -import { DEFAULT_ALERTS_INDEX, ESSENTIAL_ALERT_FIELDS } from '../../../../common/constants'; -import { getSpaceIdFromRequest } from '../helpers'; -import { securityTool } from '../constants'; - -const alertsSchema = z.object({ - query: z - .string() - .describe('A natural language query expressing the search request for security alerts'), - index: z - .string() - .optional() - .describe( - 'Specific alerts index to search against. If not provided, will search against .alerts-security.alerts-* pattern.' - ), -}); - -export const SECURITY_ALERTS_TOOL_ID = securityTool('alerts'); - -const KEEP_FIELDS = ESSENTIAL_ALERT_FIELDS.join(', '); - -const ADDITIONAL_INSTRUCTIONS = `When querying security alert indices, ALWAYS use the KEEP command to filter fields and reduce response size. Include these essential fields: ${KEEP_FIELDS}. Example: FROM .alerts-security.alerts-* | KEEP ${KEEP_FIELDS} | ...`; - -export const alertsTool = (): BuiltinToolDefinition => { - return { - id: SECURITY_ALERTS_TOOL_ID, - type: ToolType.builtin, - description: `Search and analyze security alerts using full-text or structured queries for finding, counting, aggregating, or summarizing alerts.`, - schema: alertsSchema, - handler: async ( - { query: nlQuery, index }, - { request, esClient, modelProvider, logger, events } - ) => { - // Determine the index to use: either explicitly provided or based on the current space - const spaceId = getSpaceIdFromRequest(request); - const searchIndex = index ?? `${DEFAULT_ALERTS_INDEX}-${spaceId}`; - - logger.debug( - `${SECURITY_ALERTS_TOOL_ID} tool called with query: ${nlQuery}, index: ${searchIndex}` - ); - - try { - // Generate ES|QL query with automatic KEEP field filtering - const esqlResponse = await generateEsql({ - nlQuery, - index: searchIndex, - additionalInstructions: ADDITIONAL_INSTRUCTIONS, - executeQuery: true, - model: await modelProvider.getDefaultModel(), - esClient: esClient.asCurrentUser, - logger, - events, - }); - - if (esqlResponse.error) { - return { - results: [ - { - type: 'error', - data: { - message: esqlResponse.error, - }, - }, - ], - }; - } - - const results = []; - - if (esqlResponse.query) { - results.push({ - type: 'query', - data: { - esql: esqlResponse.query, - }, - }); - } - - if (esqlResponse.results) { - results.push({ - type: 'tabularData', - data: { - source: 'esql', - query: esqlResponse.query || '', - columns: esqlResponse.results.columns, - values: esqlResponse.results.values, - }, - }); - } - - if (esqlResponse.answer) { - results.push({ - type: 'other', - data: { - answer: esqlResponse.answer, - }, - }); - } - - return { results }; - } catch (error) { - logger.error(`Error in ${SECURITY_ALERTS_TOOL_ID} tool: ${error.message}`); - return { - results: [ - { - type: 'error', - data: { - message: `Error: ${error.message}`, - }, - }, - ], - }; - } - }, - tags: ['security', 'alerts'], - }; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts deleted file mode 100644 index a738e55307d6e..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/evaluate_alert_tool.ts +++ /dev/null @@ -1,494 +0,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 Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod'; -import { ToolType } from '@kbn/onechat-common'; -import { HumanMessage } from '@langchain/core/messages'; -import type { BuiltinToolDefinition, ScopedModel, ToolEventEmitter } from '@kbn/onechat-server'; -import type { Logger } from '@kbn/logging'; -import type { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; -import { generateEsql } from '@kbn/onechat-genai-utils/tools'; -import { runSearchTool } from '@kbn/onechat-genai-utils/tools/search/run_search_tool'; -import { ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX } from '@kbn/elastic-assistant-common'; -import { DEFAULT_ALERTS_INDEX, ESSENTIAL_ALERT_FIELDS } from '../../../../common/constants'; -import { getRiskIndex } from '../../../../common/search_strategy/security_solution/risk_score/common'; -import { getSpaceIdFromRequest } from '../helpers'; -import { securityTool } from '../constants'; - -const evaluateAlertSchema = z.object({ - alertData: z - .string() - .describe( - 'The filtered alert data in key-value format (comma-separated, newline-delimited). Contains entities like host.name, user.name, source.ip, destination.ip, file.hash.sha256, file.name, file.path, service.name, kibana.alert.uuid, etc.' - ), -}); - -export const EVALUATE_ALERT_TOOL_ID = securityTool('evaluate_alert'); - -// Essential fields to keep when querying alerts to minimize token usage -const KEEP_FIELDS = ESSENTIAL_ALERT_FIELDS.join(', '); - -const ENTITY_EXTRACTION_PROMPT = `Extract security entities from the following alert data. Return a JSON object with the following structure: -{ - "alertId": "string or null", - "hostNames": ["string"], - "userNames": ["string"], - "sourceIps": ["string"], - "destinationIps": ["string"], - "fileHashes": ["string"], - "fileNames": ["string"], - "filePaths": ["string"], - "serviceNames": ["string"], - "mitreTechniques": ["string"], - "ruleName": "string or null" -} - -Extract all values found in the alert data. If a field is not present, use an empty array [] or null. -Only include non-null, non-empty values. - -Alert data: -{alertData} - -Return only valid JSON, no other text:`; - -/** - * Uses LLM to extract entities from alert data - */ -const extractEntitiesWithLLM = async ( - alertData: string, - model: ScopedModel, - logger: Logger -): Promise<{ - alertId?: string; - hostNames: string[]; - userNames: string[]; - sourceIps: string[]; - destinationIps: string[]; - fileHashes: string[]; - fileNames: string[]; - filePaths: string[]; - serviceNames: string[]; - mitreTechniques: string[]; - ruleName?: string; -}> => { - try { - const prompt = ENTITY_EXTRACTION_PROMPT.replace('{alertData}', alertData); - const response = await model.chatModel.invoke([new HumanMessage(prompt)]); - - const responseText = - typeof response.content === 'string' - ? response.content - : Array.isArray(response.content) - ? response.content.map((c) => (typeof c === 'string' ? c : c.text || '')).join('') - : String(response.content); - - // Extract JSON from response (in case LLM adds extra text) - const jsonMatch = responseText.match(/\{[\s\S]*\}/); - if (!jsonMatch) { - throw new Error('No JSON found in LLM response'); - } - - const entities = JSON.parse(jsonMatch[0]) as { - alertId?: string | null; - hostNames?: string[]; - userNames?: string[]; - sourceIps?: string[]; - destinationIps?: string[]; - fileHashes?: string[]; - fileNames?: string[]; - filePaths?: string[]; - serviceNames?: string[]; - mitreTechniques?: string[]; - ruleName?: string | null; - }; - logger.debug(`Extracted entities: ${JSON.stringify(entities, null, 2)}`); - - return { - alertId: entities.alertId || undefined, - hostNames: entities.hostNames || [], - userNames: entities.userNames || [], - sourceIps: entities.sourceIps || [], - destinationIps: entities.destinationIps || [], - fileHashes: entities.fileHashes || [], - fileNames: entities.fileNames || [], - filePaths: entities.filePaths || [], - serviceNames: entities.serviceNames || [], - mitreTechniques: entities.mitreTechniques || [], - ruleName: entities.ruleName || undefined, - }; - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.warn(`Error extracting entities with LLM: ${errorMessage}`); - // Return empty entities on error - return { - hostNames: [], - userNames: [], - sourceIps: [], - destinationIps: [], - fileHashes: [], - fileNames: [], - filePaths: [], - serviceNames: [], - mitreTechniques: [], - }; - } -}; - -/** - * Queries related alerts using generateEsql tool - */ -const queryRelatedAlerts = async ( - entities: { - hostNames: string[]; - userNames: string[]; - sourceIps: string[]; - destinationIps: string[]; - fileHashes: string[]; - }, - spaceId: string, - model: ScopedModel, - esClient: IScopedClusterClient, - logger: Logger, - events: ToolEventEmitter -): Promise => { - try { - const conditions: string[] = []; - - if (entities.hostNames.length > 0) { - conditions.push(`host.name is "${entities.hostNames[0]}"`); - } - if (entities.userNames.length > 0) { - conditions.push(`user.name is "${entities.userNames[0]}"`); - } - if (entities.sourceIps.length > 0) { - conditions.push(`source.ip is "${entities.sourceIps[0]}"`); - } - if (entities.destinationIps.length > 0) { - conditions.push(`destination.ip is "${entities.destinationIps[0]}"`); - } - if (entities.fileHashes.length > 0) { - conditions.push(`file.hash.sha256 is "${entities.fileHashes[0]}"`); - } - - if (conditions.length === 0) { - return 'No related alerts found (no entities to search for).'; - } - - const index = `${DEFAULT_ALERTS_INDEX}-${spaceId}`; - const nlQuery = `Find related security alerts from the last 7 days where ${conditions.join( - ' OR ' - )}. Include only essential fields: ${KEEP_FIELDS}. Limit to 50 results.`; - - const additionalInstructions = `When querying security alert indices, ALWAYS use the KEEP command to filter fields and reduce response size. Include these essential fields: ${KEEP_FIELDS}. Example: FROM ${index} | KEEP ${KEEP_FIELDS} | ...`; - - logger.debug(`Querying related alerts with natural language: ${nlQuery}`); - - const esqlResponse = await generateEsql({ - nlQuery, - index, - additionalInstructions, - executeQuery: true, - model, - esClient: esClient.asCurrentUser, - logger, - events, - }); - - if (esqlResponse.error) { - return `Error querying related alerts: ${esqlResponse.error}`; - } - - if (esqlResponse.results && esqlResponse.results.values.length > 0) { - const count = esqlResponse.results.values.length; - return `Found ${count} related alerts. Query: ${ - esqlResponse.query || 'N/A' - }. Results summary: ${esqlResponse.answer || 'See query results.'}`; - } - - return 'No related alerts found.'; - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.warn(`Error querying related alerts: ${errorMessage}`); - return `Error querying related alerts: ${errorMessage}`; - } -}; - -/** - * Queries risk scores using search tool - */ -const queryRiskScores = async ( - entities: { - hostNames: string[]; - userNames: string[]; - }, - spaceId: string, - model: ScopedModel, - esClient: IScopedClusterClient, - logger: Logger, - events: ToolEventEmitter -): Promise => { - try { - const riskInfo: string[] = []; - const riskIndex = getRiskIndex(spaceId, true); - - if (entities.hostNames.length > 0) { - const nlQuery = `Find risk scores for hosts: ${entities.hostNames.join( - ', ' - )}. Include host.name, host.risk.calculated_score_norm, and host.risk.calculated_level fields.`; - const results = await runSearchTool({ - nlQuery, - index: riskIndex, - model, - esClient: esClient.asCurrentUser, - logger, - events, - }); - - if (results.length > 0 && !results.some((r) => r.type === 'error')) { - riskInfo.push( - `Host Risk Scores: Found risk information for ${entities.hostNames.length} host(s).` - ); - } - } - - if (entities.userNames.length > 0) { - const nlQuery = `Find risk scores for users: ${entities.userNames.join( - ', ' - )}. Include user.name, user.risk.calculated_score_norm, and user.risk.calculated_level fields.`; - const results = await runSearchTool({ - nlQuery, - index: riskIndex, - model, - esClient: esClient.asCurrentUser, - logger, - events, - }); - - if (results.length > 0 && !results.some((r) => r.type === 'error')) { - riskInfo.push( - `User Risk Scores: Found risk information for ${entities.userNames.length} user(s).` - ); - } - } - - return riskInfo.length > 0 ? riskInfo.join('\n\n') : 'No risk score information found.'; - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.warn(`Error querying risk scores: ${errorMessage}`); - return `Error querying risk scores: ${errorMessage}`; - } -}; - -/** - * Queries attack discoveries using search tool - */ -const queryAttackDiscoveries = async ( - alertId: string | undefined, - spaceId: string, - model: ScopedModel, - esClient: IScopedClusterClient, - logger: Logger, - events: ToolEventEmitter -): Promise => { - if (!alertId) { - return 'No alert ID available to query attack discoveries.'; - } - - try { - // Query both scheduled and ad-hoc attack discovery indices - const indexPattern = [ - `${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}*`, - `.ds-.adhoc${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}*`, - `.internal${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}*`, - `.internal.ds-.adhoc${ATTACK_DISCOVERY_ALERTS_COMMON_INDEX_PREFIX}-${spaceId}*`, - ].join(','); - - const nlQuery = `Find attack discoveries that include alert ID "${alertId}" in the kibana.alert.attack_discovery.alert_ids field. Include kibana.alert.attack_discovery.title, kibana.alert.attack_discovery.summary_markdown, and kibana.alert.attack_discovery.alert_ids fields. Limit to 5 results.`; - - const results = await runSearchTool({ - nlQuery, - index: indexPattern, - model, - esClient: esClient.asCurrentUser, - logger, - events, - }); - - if (results.length > 0 && !results.some((r) => r.type === 'error')) { - return `This alert is part of one or more attack discoveries. Found attack discovery information.`; - } - - return 'This alert is not part of any known attack discovery.'; - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.warn(`Error querying attack discoveries: ${errorMessage}`); - return `Error querying attack discoveries: ${errorMessage}`; - } -}; - -/** - * Queries Security Labs knowledge base for relevant information - * Note: kbDataClient is not available in tool handler context, so this is a placeholder - * that can be enhanced if knowledge base access is added to the context in the future - */ -const querySecurityLabs = async (entities: unknown, logger: Logger): Promise => { - // Security Labs knowledge base querying is not currently available in the tool handler context - // This can be enhanced in the future if kbDataClient is added to ToolHandlerContext - logger.debug('Security Labs knowledge base querying not available in tool handler context'); - return 'Security Labs knowledge base querying is not currently available. Consider using the Security Labs tool separately if needed.'; -}; - -const EVALUATION_PROMPT = `You are a security analyst evaluating a security alert with enriched context. Analyze the alert data and all provided context to generate a comprehensive evaluation report. - -SECURITY ALERT DATA: - -{alertData} - ---- - -ENRICHED CONTEXT: - -RELATED ALERTS: -{relatedAlerts} - -RISK SCORES: -{riskScores} - -ATTACK DISCOVERIES: -{attackDiscoveries} - -SECURITY LABS: -{securityLabs} - ---- - -EVALUATION REQUIREMENTS - -Evaluate the security event described above and provide a structured, markdown-formatted summary suitable for inclusion in an Elastic Security case. Use the enriched context above to provide a comprehensive analysis. Your response must include: - -1. 📝 Event Description - - Summarize the event using information from the alert data and enriched context. - - Include user and host information, risk scores, and severity from the provided context. - - Reference relevant MITRE ATT&CK techniques based on the event details, with hyperlinks to the official MITRE pages. - - If this alert is part of an attack discovery, highlight that context. - -2. 🔍 Triage Steps - - List clear, bulleted triage steps tailored to Elastic Security workflows (e.g., alert investigation, timeline creation, entity analytics review). - - Base recommendations on the alert fields and related alerts found (e.g., host.name, user.name, source.ip, destination.ip). - - Highlight the detection rule mentioned in the alert data. - - If related alerts were found, mention patterns or trends observed. - -3. 🛡️ Recommended Actions - - Provide prioritized response actions based on the alert data and context: - - Elastic Defend endpoint response actions (e.g., isolate host, kill process, retrieve/delete file), with links to Elastic documentation. - - Example ES|QL queries for further investigation using the fields from the alert (host.name, user.name, IPs, timestamps), formatted as code blocks. - - Example OSQuery Manager queries for further investigation, formatted as code blocks. - - Guidance on using Timelines and Entity Analytics for deeper context, with documentation links. - - If risk scores indicate high-risk entities, recommend immediate investigation. - -4. 📚 MITRE ATT&CK Context - - Analyze the event category and rule description to identify relevant MITRE ATT&CK techniques. - - Provide actionable recommendations based on MITRE guidance, with hyperlinks. - - If Security Labs articles were found, reference relevant research and findings. - -5. 🔗 Documentation Links - - Include direct links to all referenced Elastic Security documentation and MITRE ATT&CK pages. - - If Security Labs articles were found, include links to those articles. - -Formatting Requirements: - - Use markdown headers, tables, and code blocks for clarity. - - Organize the response into visually distinct sections. - - Use concise, actionable language. - - Include relevant emojis in section headers for visual clarity (e.g., 📝, 🛡️, 🔍, 📚). - -Generate the complete evaluation report now:`; - -export const evaluateAlertTool = (): BuiltinToolDefinition => { - return { - id: EVALUATE_ALERT_TOOL_ID, - type: ToolType.builtin, - description: `Evaluates a security alert with enriched context by querying related alerts, risk scores, attack discoveries, and Security Labs content. Generates a comprehensive, structured markdown report suitable for inclusion in an Elastic Security case. - -CRITICAL INSTRUCTION: This tool returns a COMPLETE FINAL ANSWER in the 'answer' field. You MUST return this answer EXACTLY as-is without any modification, summarization, or additional commentary. Copy the entire 'answer' field content verbatim and return it directly to the user. Do NOT synthesize, summarize, or rephrase this content.`, - schema: evaluateAlertSchema, - handler: async ({ alertData }, { request, esClient, modelProvider, logger, events }) => { - logger.debug( - `${EVALUATE_ALERT_TOOL_ID} tool called with alert data length: ${alertData.length}` - ); - - try { - const spaceId = getSpaceIdFromRequest(request); - const model = await modelProvider.getDefaultModel(); - - // Extract entities using LLM - const entities = await extractEntitiesWithLLM(alertData, model, logger); - logger.debug(`Extracted entities: ${JSON.stringify(entities, null, 2)}`); - - // Query related alerts, risk scores, attack discoveries, and security labs in parallel - const [relatedAlerts, riskScores, attackDiscoveries, securityLabs] = await Promise.all([ - queryRelatedAlerts(entities, spaceId, model, esClient, logger, events), - queryRiskScores(entities, spaceId, model, esClient, logger, events), - queryAttackDiscoveries(entities.alertId, spaceId, model, esClient, logger, events), - querySecurityLabs(entities, logger), - ]); - - // Build enriched context - const enrichedContext = { - relatedAlerts, - riskScores, - attackDiscoveries, - securityLabs, - }; - - logger.debug(`Enriched context gathered: ${JSON.stringify(enrichedContext, null, 2)}`); - - // Generate evaluation using LLM - const prompt = EVALUATION_PROMPT.replace('{alertData}', alertData) - .replace('{relatedAlerts}', relatedAlerts) - .replace('{riskScores}', riskScores) - .replace('{attackDiscoveries}', attackDiscoveries) - .replace('{securityLabs}', securityLabs); - - const response = await model.chatModel.invoke([new HumanMessage(prompt)]); - - const evaluationText = - typeof response.content === 'string' - ? response.content - : Array.isArray(response.content) - ? response.content.map((c) => (typeof c === 'string' ? c : c.text || '')).join('') - : String(response.content); - - return { - results: [ - { - type: 'other', - data: { - answer: evaluationText, - _verbatim: true, - _finalAnswer: true, - }, - }, - ], - }; - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error(`Error in ${EVALUATE_ALERT_TOOL_ID} tool: ${errorMessage}`); - return { - results: [ - { - type: 'error', - data: { - message: `Error evaluating alert: ${errorMessage}`, - }, - }, - ], - }; - } - }, - tags: ['security', 'alerts', 'evaluation'], - }; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/helpers.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/helpers.ts deleted file mode 100644 index c4c5c1a8f018f..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/alerts/helpers.ts +++ /dev/null @@ -1,26 +0,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 Elastic License - * 2.0. - */ - -import { DEFAULT_SPACE_ID } from '@kbn/spaces-utils'; -import type { KibanaRequest } from '@kbn/core/server'; - -/** - * Extracts the space ID from the request. - * Falls back to extracting from basePath if spaces plugin is not available. - */ -export const getSpaceIdFromRequest = (request: KibanaRequest): string => { - // Try to extract from request URL path (space context is in the path as /s/{spaceId}) - const pathname = request.url?.pathname || ''; - const spaceMatch = pathname.match(/^\/s\/([a-z0-9_\-]+)/); - - if (spaceMatch && spaceMatch[1]) { - return spaceMatch[1]; - } - - // Fallback to default space - return DEFAULT_SPACE_ID; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts index 15872a08537bd..b4547e0d22119 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts @@ -6,7 +6,7 @@ */ import { z } from '@kbn/zod'; -import { ToolType } from '@kbn/onechat-common'; +import { ToolType, ToolResultType } from '@kbn/onechat-common'; import type { BuiltinToolDefinition } from '@kbn/onechat-server'; import { executeEsql } from '@kbn/onechat-genai-utils'; import { getSpaceIdFromRequest } from './helpers'; @@ -67,13 +67,13 @@ export const attackDiscoverySearchTool = (): BuiltinToolDefinition< const results = [ { - type: 'query' as const, + type: ToolResultType.query, data: { esql: esqlQuery, }, }, { - type: 'tabularData' as const, + type: ToolResultType.tabularData, data: { source: 'esql', query: esqlQuery, @@ -89,7 +89,7 @@ export const attackDiscoverySearchTool = (): BuiltinToolDefinition< return { results: [ { - type: 'error', + type: ToolResultType.error, data: { message: `Error: ${error.message}`, }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts index 4163a48437554..8b5e83f364922 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts @@ -6,7 +6,7 @@ */ import { z } from '@kbn/zod'; -import { ToolType } from '@kbn/onechat-common'; +import { ToolType, ToolResultType } from '@kbn/onechat-common'; import type { BuiltinToolDefinition } from '@kbn/onechat-server'; import { runSearchTool } from '@kbn/onechat-genai-utils/tools/search/run_search_tool'; import { SECURITY_LABS_RESOURCE } from '@kbn/elastic-assistant-plugin/server/routes/knowledge_base/constants'; @@ -54,7 +54,7 @@ export const securityLabsSearchTool = (): BuiltinToolDefinition< return { results: [ { - type: 'error', + type: ToolResultType.error, data: { message: `Error: ${error.message}`, }, From a30c59c800923f315e73fa34453738fca15e52bd Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 15:17:46 -0700 Subject: [PATCH 44/96] risk_entity => entity_risk --- .../attachments/{risk_entity.ts => entity_risk.ts} | 12 ++++++------ .../server/agent_builder/attachments/index.ts | 3 +-- .../plugins/security_solution/server/plugin.ts | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) rename x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/{risk_entity.ts => entity_risk.ts} (89%) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts similarity index 89% rename from x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts rename to x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts index a0b5a9136b1bc..f3633ee04076a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/risk_entity.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts @@ -19,12 +19,12 @@ const riskEntityAttachmentDataSchema = z.object({ * Data for a risk entity attachment. * Note: After validation, the data is stored as a formatted string. */ -type RiskEntityAttachmentData = z.infer; +type EntityRiskAttachmentData = z.infer; /** * Type guard to check if data is a formatted risk entity string */ -const isRiskEntityFormattedData = (data: unknown): data is string => { +const isEntityRiskFormattedData = (data: unknown): data is string => { return ( typeof data === 'string' && data.includes('identifier:') && data.includes('identifierType:') ); @@ -33,13 +33,13 @@ const isRiskEntityFormattedData = (data: unknown): data is string => { /** * Creates the definition for the `risk_entity` attachment type. */ -export const createRiskEntityAttachmentType = (): AttachmentTypeDefinition => { +export const createEntityRiskAttachmentType = (): AttachmentTypeDefinition => { return { id: SecurityAgentBuilderAttachments.risk_entity, validate: (input) => { const parseResult = riskEntityAttachmentDataSchema.safeParse(input); if (parseResult.success) { - return { valid: true, data: formatRiskEntityData(parseResult.data) }; + return { valid: true, data: formatEntityRiskData(parseResult.data) }; } else { return { valid: false, error: parseResult.error.message }; } @@ -49,7 +49,7 @@ export const createRiskEntityAttachmentType = (): AttachmentTypeDefinition => { const data = attachment.data; // Necessary because we cannot currently use the AttachmentType type as agent is not // registered with enum AttachmentType in onechat attachment_types.ts - if (!isRiskEntityFormattedData(data)) { + if (!isEntityRiskFormattedData(data)) { throw new Error(`Invalid risk entity attachment data for attachment ${attachment.id}`); } return { @@ -82,6 +82,6 @@ CRITICAL: You MUST call ${sanitizeToolId( }; }; -const formatRiskEntityData = (data: RiskEntityAttachmentData): string => { +const formatEntityRiskData = (data: EntityRiskAttachmentData): string => { return `identifier: ${data.identifier}, identifierType: ${data.identifierType}`; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts index 4bccfd85feb1c..7c4a9731e44e7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts @@ -7,6 +7,5 @@ export { createAlertAttachmentType } from './alert'; export { createAttackDiscoveryAttachmentType } from './attack_discovery'; -export { createRiskEntityAttachmentType } from './risk_entity'; +export { createEntityRiskAttachmentType } from './entity_risk'; export { createQueryHelpAttachmentType } from './query_help'; -// export { createAlertAttachmentType } from './core-alert'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index e74be53c81b6c..1a74cbffb4f2c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -149,7 +149,7 @@ import type { TelemetryQueryConfiguration } from './lib/telemetry/types'; import { createAlertAttachmentType, createAttackDiscoveryAttachmentType, - createRiskEntityAttachmentType, + createEntityRiskAttachmentType, createQueryHelpAttachmentType, } from './agent_builder/attachments'; import { @@ -246,7 +246,7 @@ export class Plugin implements ISecuritySolutionPlugin { onechat.attachments.registerType(createAlertAttachmentType()); onechat.attachments.registerType(createAttackDiscoveryAttachmentType()); - onechat.attachments.registerType(createRiskEntityAttachmentType()); + onechat.attachments.registerType(createEntityRiskAttachmentType()); onechat.attachments.registerType(createQueryHelpAttachmentType()); // Register tools From fc8eefeff57eccbed36fa1e47820c3b80d3d7d5d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 15:33:24 -0700 Subject: [PATCH 45/96] simplify --- .../tools/builtin/definitions/cases/cases.ts | 306 +++++++++--------- .../builtin/definitions/cases/helpers.ts | 252 ++++----------- 2 files changed, 207 insertions(+), 351 deletions(-) diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts index 88a6fdadc0ff7..eb413afe45f53 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts @@ -11,19 +11,21 @@ import type { BuiltinToolDefinition } from '@kbn/onechat-server'; import type { CoreSetup } from '@kbn/core/server'; import type { Case, RelatedCase } from '@kbn/cases-plugin/common/types/domain'; import type { CasesSearchRequest } from '@kbn/cases-plugin/common/types/api'; +import type { CasesClient } from '@kbn/cases-plugin/server/client'; +import type { KibanaRequest } from '@kbn/core-http-server'; +import type { Logger } from '@kbn/logging'; import type { OnechatStartDependencies, OnechatPluginStart } from '../../../../../types'; import { normalizeTimeRange, - createEmptyResults, createCommentSummariesFromArray, - fetchCommentsForCases, enhanceCaseData, - createToolResult, - enhanceCasesWithComments, + createResult, createErrorResponse, getCasesClient, deduplicateCases, + fetchAllPages, type CoreServices, + type EnhancedCaseData, } from './helpers'; const casesSchema = z.object({ @@ -123,6 +125,103 @@ const casesSchema = z.object({ ), }); +/** + * Fetches and enhances a single case by ID. + */ +async function fetchCaseById( + caseId: string, + casesClient: CasesClient, + includeComments: boolean, + request: KibanaRequest, + coreServices: CoreServices, + logger: Logger +): Promise { + const theCase = await casesClient.cases.get({ + id: caseId, + includeComments, + }); + + const commentsSummary = + includeComments && theCase.comments + ? createCommentSummariesFromArray(theCase.comments) + : undefined; + + return enhanceCaseData(theCase, commentsSummary, request, coreServices, logger); +} + +/** + * Fetches cases by alert IDs. + */ +async function fetchCasesByAlertIds( + alertIds: string[], + casesClient: CasesClient, + owner: string | undefined, + includeComments: boolean, + logger: Logger +): Promise { + // Query each alert ID in parallel + const allRelatedCasesArrays = await Promise.all( + alertIds.map(async (alertId) => { + try { + return await casesClient.cases.getCasesByAlertID({ + alertID: alertId, + options: owner ? { owner } : {}, + }); + } catch (error) { + logger.warn(`[Cases Tool] Failed to fetch cases for alert ID ${alertId}: ${error}`); + return []; + } + }) + ); + + // Flatten and deduplicate cases by case ID + const relatedCases: RelatedCase[] = deduplicateCases(allRelatedCasesArrays); + + if (relatedCases.length === 0) { + return []; + } + + // Fetch full case details for each related case + const caseFetchResults = await Promise.allSettled( + relatedCases.map((relatedCase) => + casesClient.cases.get({ + id: relatedCase.id, + includeComments, + }) + ) + ); + + return caseFetchResults.flatMap((result, index) => { + if (result.status === 'fulfilled') { + return [result.value]; + } + logger.warn( + `[Cases Tool] Failed to fetch full details for case ${relatedCases[index].id}: ${result.reason}` + ); + return []; + }); +} + +/** + * Enhances an array of cases with comments and formatting. + */ +function enhanceCases( + cases: Case[], + includeComments: boolean, + request: KibanaRequest, + coreServices: CoreServices, + logger: Logger +): EnhancedCaseData[] { + return cases.map((theCase) => { + const commentsSummary = + includeComments && theCase.comments + ? createCommentSummariesFromArray(theCase.comments) + : undefined; + + return enhanceCaseData(theCase, commentsSummary, request, coreServices, logger); + }); +} + export const casesTool = ( coreSetup: CoreSetup ): BuiltinToolDefinition => { @@ -191,144 +290,70 @@ Returns case details (id, title, description, status, severity, tags, assignees, { request, logger } ) => { try { - // Get start services once at the beginning const [coreStart, pluginsStart] = await coreSetup.getStartServices(); const coreServices: CoreServices = { coreStart, spacesPlugin: pluginsStart.spaces, }; - // Normalize and adjust time range using provided start/end parameters - // Returns null if no time range is provided const timeRange = normalizeTimeRange(start, end, logger); - - // Get cases client const casesClientResult = await getCasesClient(pluginsStart, request, logger, timeRange); if ('error' in casesClientResult) { return casesClientResult.error; } const { casesClient } = casesClientResult; + const shouldIncludeComments = includeComments ?? false; // Operation mode 1: Get case by ID if (caseId) { - try { - logger.info(`[Cases Tool] Getting case by ID: ${caseId}`); - const theCase = await casesClient.cases.get({ - id: caseId, - includeComments: includeComments ?? false, - }); - - const commentsSummary = - includeComments && theCase.comments - ? createCommentSummariesFromArray(theCase.comments) - : []; - - const caseData = enhanceCaseData( - theCase, - commentsSummary, - theCase.totalComment || 0, - request, - coreServices, - logger - ); - - return createToolResult([caseData], null, `Retrieved case: ${theCase.title}`); - } catch (error) { - return createErrorResponse( - error, - `[Cases Tool] Error fetching case by ID ${caseId}`, - `Error fetching case ${caseId}`, - logger - ); - } + logger.info(`[Cases Tool] Getting case by ID: ${caseId}`); + const caseData = await fetchCaseById( + caseId, + casesClient, + shouldIncludeComments, + request, + coreServices, + logger + ); + return createResult([caseData], null, `Retrieved case: ${caseData.title}`); } - // Operation mode 2: Find cases by alert ID(s) - const finalAlertIds = alertIds && alertIds.length > 0 ? alertIds : undefined; - if (finalAlertIds && finalAlertIds.length > 0) { - try { - logger.info(`[Cases Tool] Querying cases by alert IDs: ${finalAlertIds.join(', ')}`); - // Query each alert ID in parallel - const allRelatedCasesArrays = await Promise.all( - finalAlertIds.map(async (alertId) => { - try { - return await casesClient.cases.getCasesByAlertID({ - alertID: alertId, - options: owner ? { owner } : {}, - }); - } catch (error) { - logger.warn( - `[Cases Tool] Failed to fetch cases for alert ID ${alertId}: ${error}` - ); - return []; - } - }) - ); - - // Flatten and deduplicate cases by case ID - const relatedCases: RelatedCase[] = deduplicateCases(allRelatedCasesArrays); - - if (relatedCases.length === 0) { - return createEmptyResults( - timeRange?.start || null, - timeRange?.end || null, - `No cases found containing alert IDs: ${finalAlertIds.join(', ')}` - ); - } - - // Fetch full case details for each related case - const caseFetchResults = await Promise.allSettled( - relatedCases.map((relatedCase) => - casesClient.cases.get({ - id: relatedCase.id, - includeComments: false, - }) - ) - ); - - const casesWithDetails = caseFetchResults.flatMap((result, index) => { - if (result.status === 'fulfilled') { - return [result.value]; - } - logger.warn( - `[Cases Tool] Failed to fetch full details for case ${relatedCases[index].id}: ${result.reason}` - ); - return []; - }); - - const casesWithComments = await fetchCommentsForCases( - casesWithDetails, - casesClient, - includeComments, - logger - ); - - const casesData = enhanceCasesWithComments( - casesWithComments, - coreServices, - request, - logger - ); - - return createToolResult( - casesData, + // Operation mode 2: Find cases by alert IDs + if (alertIds && alertIds.length > 0) { + logger.info(`[Cases Tool] Querying cases by alert IDs: ${alertIds.join(', ')}`); + const cases = await fetchCasesByAlertIds( + alertIds, + casesClient, + owner, + shouldIncludeComments, + logger + ); + + if (cases.length === 0) { + return createResult( + [], timeRange, - `Found ${ - casesData.length - } unique case(s) containing alert ID(s): ${finalAlertIds.join(', ')}` - ); - } catch (error) { - return createErrorResponse( - error, - `[Cases Tool] Error fetching cases by alert IDs ${finalAlertIds.join(', ')}`, - `Error fetching cases for alert IDs ${finalAlertIds.join(', ')}`, - logger + `No cases found containing alert IDs: ${alertIds.join(', ')}` ); } + + const casesData = enhanceCases( + cases, + shouldIncludeComments, + request, + coreServices, + logger + ); + return createResult( + casesData, + timeRange, + `Found ${casesData.length} unique case(s) containing alert ID(s): ${alertIds.join( + ', ' + )}` + ); } - // Operation mode 3: Search cases using Cases API - // Build search parameters from schema parameters + // Operation mode 3: Search cases const searchParams: CasesSearchRequest = { sortField: 'updatedAt', sortOrder: 'desc', @@ -347,46 +372,15 @@ Returns case details (id, title, description, status, severity, tags, assignees, ...(timeRange?.end && { to: timeRange.end }), }; - // Fetch cases with pagination - const allCases: Case[] = []; - let currentPage = 1; - const maxPages = 10; - let hasMorePages = true; - - while (hasMorePages && currentPage <= maxPages) { - searchParams.page = currentPage; - const searchResult = await casesClient.cases.search(searchParams); - - if (searchResult.cases.length === 0) { - hasMorePages = false; - break; - } - - allCases.push(...searchResult.cases); - - // Check if we should continue fetching - if (searchResult.cases.length < (searchParams.perPage ?? 100)) { - hasMorePages = false; - } - - currentPage++; - } - - const casesWithComments = await fetchCommentsForCases( + const allCases = await fetchAllPages(casesClient, searchParams); + const casesData = enhanceCases( allCases, - casesClient, - includeComments, - logger - ); - - const casesData = enhanceCasesWithComments( - casesWithComments, - coreServices, + shouldIncludeComments, request, + coreServices, logger ); - - return createToolResult(casesData, timeRange); + return createResult(casesData, timeRange); } catch (error) { return createErrorResponse( error, diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/helpers.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/helpers.ts index e347a1dfca38a..45b5f98722c0c 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/helpers.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/helpers.ts @@ -18,6 +18,7 @@ import type { RelatedCase, UserCommentAttachment, } from '@kbn/cases-plugin/common/types/domain'; +import type { CasesSearchRequest } from '@kbn/cases-plugin/common/types/api'; import type { OnechatStartDependencies } from '../../../../../types'; import { getCaseUrl } from '../../../../../utils/case_urls'; @@ -28,38 +29,20 @@ export interface CommentSummary { created_at: string | null; } -export interface CommentFetchResult { - case: Case; - comments: CommentSummary[]; - totalComments: number; -} - export interface CoreServices { coreStart: CoreStart; spacesPlugin: SpacesPluginStart | undefined; } -export interface EnhancedCaseData - extends Omit< - Case, - 'assignees' | 'observables' | 'totalAlerts' | 'totalComment' | 'created_by' | 'updated_by' - > { +export interface EnhancedCaseData extends Case { url: string | null; markdown_link: string; - assignees: string[]; - observables_count: number; - observables: Array<{ type: string | null; value: string | null }>; - total_alerts: number; - total_comments: number; - created_by: string | null; - updated_by: string | null; - comments_summary: CommentSummary[]; + comments_summary?: CommentSummary[]; } /** * Normalizes and validates time range parameters for case queries. - * Handles date strings that may or may not include a year (assumes current year if missing). - * Validates dates and logs warnings for invalid dates. + * Validates ISO date strings and logs warnings for invalid dates. * * @param start - ISO datetime string for start time (inclusive), optional * @param end - ISO datetime string for end time (exclusive), optional @@ -76,20 +59,13 @@ export const normalizeTimeRange = ( startDate: Date | null; endDate: Date | null; } | null => { - // If neither start nor end is provided, return null to indicate no time range filtering if (!start && !end) { return null; } - const now = new Date(); - const currentYear = now.getFullYear(); - let startDate: Date | null = null; if (start) { - // If no year is specified, assume current year - const startStr = - start.includes('T') && !start.match(/^\d{4}/) ? `${currentYear}-${start}` : start; - startDate = new Date(startStr); + startDate = new Date(start); if (isNaN(startDate.getTime())) { logger.warn(`Invalid start date: ${start}`); startDate = null; @@ -98,8 +74,7 @@ export const normalizeTimeRange = ( let endDate: Date | null = null; if (end) { - const endStr = end.includes('T') && !end.match(/^\d{4}/) ? `${currentYear}-${end}` : end; - endDate = new Date(endStr); + endDate = new Date(end); if (isNaN(endDate.getTime())) { logger.warn(`Invalid end date: ${end}`); endDate = null; @@ -115,44 +90,33 @@ export const normalizeTimeRange = ( }; /** - * Creates an empty results response for the cases tool. - * Used when no cases are found or when the cases plugin is unavailable. + * Creates a standardized tool result response for the cases tool. + * Handles success, empty, and error cases. * - * @param normalizedStart - Normalized start time ISO string, or null - * @param normalizedEnd - Normalized end time ISO string, or null - * @param message - Message explaining why results are empty - * @returns Tool result object with empty cases array and the provided message + * @param cases - Array of enhanced case data objects (empty array for empty results) + * @param timeRange - Normalized time range object or null if no time filtering + * @param message - Optional message to include in the response + * @returns Tool result object conforming to ToolResultType.other format */ -export const createEmptyResults = ( - normalizedStart: string | null, - normalizedEnd: string | null, - message: string +export const createResult = ( + cases: EnhancedCaseData[], + timeRange: ReturnType | null, + message?: string ) => ({ results: [ { type: ToolResultType.other, data: { - cases: [], - total: 0, - start: normalizedStart || null, - end: normalizedEnd || null, - message, + total: cases.length, + cases, + start: timeRange?.start || null, + end: timeRange?.end || null, + ...(message && { message }), }, }, ], }); -/** - * Extracts a human-readable error message from an error object. - * Handles both Error instances and other error types. - * - * @param error - The error object of unknown type - * @returns The error message string - */ -export const extractErrorMessage = (error: unknown): string => { - return error instanceof Error ? error.message : String(error); -}; - /** * Creates a summary object from a case attachment/comment. * Extracts key information including comment text (truncated to 200 chars), @@ -189,95 +153,57 @@ export const createCommentSummariesFromArray = (comments: Attachment[]): Comment }; /** - * Fetches comments for a single case using the cases client. - * Retrieves up to 10 comments sorted by creation date (descending). - * Returns empty comments array if fetch fails, but still includes the case. + * Fetches all pages of cases from a search query. + * Handles pagination automatically up to a maximum number of pages. * - * @param caseItem - The case object to fetch comments for * @param casesClient - The cases client instance for API calls - * @param logger - Logger instance for error logging - * @returns Promise resolving to CommentFetchResult with case, comments, and total count + * @param searchParams - Search parameters for the query + * @param maxPages - Maximum number of pages to fetch (default: 10) + * @returns Promise resolving to array of all cases from all pages */ -export const fetchCommentsForCase = async ( - caseItem: Case, +export const fetchAllPages = async ( casesClient: CasesClient, - logger: Logger -): Promise => { - try { - const commentsResponse = await casesClient.attachments.find({ - caseID: caseItem.id, - findQueryParams: { - page: 1, - perPage: 10, - sortOrder: 'desc', - }, - }); + searchParams: CasesSearchRequest, + maxPages: number = 10 +): Promise => { + const allCases: Case[] = []; + let currentPage = 1; + let hasMorePages = true; - const commentSummaries = createCommentSummariesFromArray(commentsResponse.comments || []); + while (hasMorePages && currentPage <= maxPages) { + searchParams.page = currentPage; + const searchResult = await casesClient.cases.search(searchParams); - return { - case: caseItem, - comments: commentSummaries, - totalComments: commentsResponse.total || 0, - }; - } catch (error) { - logger.warn(`[Cases Tool] Failed to fetch comments for case ${caseItem.id}: ${error}`); - return { - case: caseItem, - comments: [], - totalComments: caseItem.totalComment || 0, - }; + if (searchResult.cases.length === 0) { + break; + } + + allCases.push(...searchResult.cases); + + // Stop if we got fewer results than requested (last page) + if (searchResult.cases.length < (searchParams.perPage ?? 100)) { + hasMorePages = false; + } + + currentPage++; } -}; -/** - * Fetches comments for multiple cases in parallel. - * If shouldFetch is false, returns cases with empty comment arrays. - * Otherwise, fetches comments for each case concurrently. - * - * @param cases - Array of case objects to fetch comments for - * @param casesClient - The cases client instance for API calls - * @param shouldFetch - Whether to actually fetch comments (false returns empty arrays) - * @param logger - Logger instance for error logging - * @returns Promise resolving to array of CommentFetchResult objects - */ -export const fetchCommentsForCases = async ( - cases: Case[], - casesClient: CasesClient, - shouldFetch: boolean, - logger: Logger -): Promise => { - return Promise.all( - cases.map(async (caseItem) => { - if (!shouldFetch) { - return { - case: caseItem, - comments: [], - totalComments: caseItem.totalComment || 0, - }; - } - return fetchCommentsForCase(caseItem, casesClient, logger); - }) - ); + return allCases; }; /** - * Enhances a case object with additional computed fields and formatting. - * Adds URL generation, markdown links, normalized assignees, observables summary, - * and comment summaries. Transforms user objects to usernames for display. + * Enhances a case object with URL and markdown link fields. * * @param caseItem - The base case object from the API - * @param comments - Array of comment summaries to include - * @param totalComments - Total number of comments for the case + * @param comments - Optional array of comment summaries to include * @param request - Kibana request object for URL generation * @param coreServices - Core services including CoreStart and SpacesPlugin * @param logger - Logger instance for warning messages - * @returns Enhanced case data object with all computed fields + * @returns Enhanced case data object with url and markdown_link fields */ export const enhanceCaseData = ( caseItem: Case, - comments: CommentSummary[], - totalComments: number, + comments: CommentSummary[] | undefined, request: KibanaRequest, coreServices: CoreServices, logger: Logger @@ -300,72 +226,12 @@ export const enhanceCaseData = ( return { ...caseItem, - assignees: caseItem.assignees?.map((a) => a.uid || String(a)) || [], - observables_count: caseItem.total_observables ?? caseItem.observables?.length ?? 0, - observables: (caseItem.observables || []).slice(0, 5).map((obs) => ({ - type: obs.typeKey || null, - value: obs.value || null, - })), - created_by: caseItem.created_by.username ?? caseItem.created_by.email ?? null, - updated_by: caseItem.updated_by?.username ?? caseItem.updated_by?.email ?? null, - total_alerts: caseItem.totalAlerts || 0, - total_comments: totalComments, - comments_summary: comments, url: caseUrl, markdown_link: markdownLink, + ...(comments && comments.length > 0 && { comments_summary: comments }), }; }; -/** - * Creates a standardized tool result response for the cases tool. - * Formats the response according to the onechat tool result specification. - * - * @param cases - Array of enhanced case data objects - * @param timeRange - Normalized time range object or null if no time filtering - * @param message - Optional message to include in the response - * @returns Tool result object conforming to ToolResultType.other format - */ -export const createToolResult = ( - cases: EnhancedCaseData[], - timeRange: ReturnType | null, - message?: string -) => ({ - results: [ - { - type: ToolResultType.other, - data: { - total: cases.length, - cases, - start: timeRange?.start || null, - end: timeRange?.end || null, - ...(message && { message }), - }, - }, - ], -}); - -/** - * Enhances multiple cases with comments by applying enhanceCaseData to each. - * Processes all cases in the array and returns enhanced versions with URLs, - * markdown links, and formatted fields. - * - * @param casesWithComments - Array of CommentFetchResult objects containing cases and their comments - * @param coreServices - Core services including CoreStart and SpacesPlugin - * @param request - Kibana request object for URL generation - * @param logger - Logger instance for warning messages - * @returns Array of enhanced case data objects - */ -export const enhanceCasesWithComments = ( - casesWithComments: CommentFetchResult[], - coreServices: CoreServices, - request: KibanaRequest, - logger: Logger -): EnhancedCaseData[] => { - return casesWithComments.map(({ case: caseItem, comments, totalComments }) => - enhanceCaseData(caseItem, comments, totalComments, request, coreServices, logger) - ); -}; - /** * Creates a standardized error response for the cases tool. * Extracts error message, logs it with the provided prefix, and returns @@ -383,7 +249,7 @@ export const createErrorResponse = ( userMessage: string, logger: Logger ) => { - const errorMessage = extractErrorMessage(error); + const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`${logPrefix}: ${errorMessage}`); return { results: [createErrorResult(`${userMessage}: ${errorMessage}`)], @@ -406,17 +272,13 @@ export const getCasesClient = async ( request: KibanaRequest, logger: Logger, timeRange: ReturnType | null -): Promise<{ casesClient: CasesClient } | { error: ReturnType }> => { +): Promise<{ casesClient: CasesClient } | { error: ReturnType }> => { const casesPlugin = pluginsStart.cases; if (!casesPlugin) { logger.warn('[Cases Tool] Cases plugin not available, returning empty results'); return { - error: createEmptyResults( - timeRange?.start || null, - timeRange?.end || null, - 'Cases plugin not available' - ), + error: createResult([], timeRange, 'Cases plugin not available'), }; } From eff1f4aa4289a66edf90e793b6ef641372691cef Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 Nov 2025 15:35:23 -0700 Subject: [PATCH 46/96] rm outdated tool refs --- .../security_solution/server/agent_builder/tools/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts index 830e99f40ed10..f249c02f2906f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/index.ts @@ -5,12 +5,6 @@ * 2.0. */ -export { alertsTool, SECURITY_ALERTS_TOOL_ID } from './alerts/alerts_tool'; -export { - alertsIndexSearchTool, - SECURITY_ALERTS_INDEX_SEARCH_TOOL_ID, -} from './alerts/alerts_index_search_tool'; -export { evaluateAlertTool, EVALUATE_ALERT_TOOL_ID } from './alerts/evaluate_alert_tool'; export { entityRiskScoreTool, SECURITY_ENTITY_RISK_SCORE_TOOL_ID } from './entity_risk_score_tool'; export { attackDiscoverySearchTool, From d4f8c0957258e9ec9997a1a4160ab328e6567e4e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Sat, 29 Nov 2025 10:22:17 -0500 Subject: [PATCH 47/96] use actual AB flyout! --- .../flyout/open_conversation_flyout.tsx | 1 + .../new_agent_builder_attachment.tsx | 1 + .../hooks/use_agent_builder_attachment.ts | 180 +++--------------- .../plugins/security_solution/public/types.ts | 2 + 4 files changed, 27 insertions(+), 157 deletions(-) diff --git a/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx b/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx index 974b95c0ae386..4104d48d70b85 100644 --- a/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx +++ b/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx @@ -71,6 +71,7 @@ export function openConversationFlyout( type: 'push', hideCloseButton: true, 'aria-labelledby': ariaLabelledBy, + maxWidth: 1200, // Maximum width for resizable flyout to prevent NaN error } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx index 024d6b6e5eb3c..f1c3668e22154 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx @@ -67,3 +67,4 @@ NewAgentBuilderAttachmentComponent.displayName = 'NewAgentBuilderAttachmentCompo * with attachment data. You may optionally override the default text. */ export const NewAgentBuilderAttachment = React.memo(NewAgentBuilderAttachmentComponent); + diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts index d93fef0c919f8..205d0460e2688 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts @@ -5,22 +5,9 @@ * 2.0. */ -import React, { useCallback, useState } from 'react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '@kbn/react-kibana-mount'; -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiText, - logicalCSS, - useEuiTheme, -} from '@elastic/eui'; -import { css } from '@emotion/react'; -import type { ChatResponse } from '@kbn/onechat-plugin/common/http_api/chat'; -import { useAppToasts } from '../../common/hooks/use_app_toasts'; -import { useAppUrl } from '../../common/lib/kibana/hooks'; +import { useCallback } from 'react'; +import type { UiAttachment } from '@kbn/onechat-plugin/public/embeddable/types'; +import { useKibana } from '../../common/lib/kibana/use_kibana'; export interface UseAgentBuilderAttachmentParams { /** @@ -39,167 +26,46 @@ export interface UseAgentBuilderAttachmentParams { export interface UseAgentBuilderAttachmentResult { /** - * Function to open the agent builder flyout - * TODO: This currently calls the API directly as a temporary implementation. - * Once the agent builder UI is ready, this will open a flyout with the attachment data instead. + * Function to open the agent builder flyout with attachments and prefilled conversation */ openAgentBuilderFlyout: () => void; - /** - * Whether the API call is in progress - */ - isLoading: boolean; } -const AgentBuilderToastSuccessContent: React.FC<{ - onViewConversationClick?: () => void; - content?: string; -}> = ({ onViewConversationClick, content }) => { - const { euiTheme } = useEuiTheme(); - return React.createElement( - React.Fragment, - null, - content !== undefined - ? React.createElement( - EuiText, - { - size: 's', - css: css` - ${logicalCSS('margin-bottom', euiTheme.size.s)}; - `, - 'data-test-subj': 'toaster-content-sync-text', - }, - content - ) - : null, - onViewConversationClick !== undefined - ? React.createElement( - EuiFlexGroup, - { justifyContent: 'flexEnd', gutterSize: 's' }, - React.createElement( - EuiFlexItem, - { grow: false }, - React.createElement( - EuiButton, - { - size: 's', - onClick: onViewConversationClick, - 'data-test-subj': 'toaster-content-conversation-view-link', - }, - i18n.translate( - 'xpack.securitySolution.agentBuilder.attachment.viewConversationButton', - { - defaultMessage: 'View conversation', - } - ) - ) - ) - ) - : null - ); -}; - /** * Hook to handle agent builder attachment functionality. - * Temporarily calls the API directly until the agent builder UI is ready. - * Eventually, this will open a flyout with the attachment data. + * Opens a conversation flyout with attachments and prefilled conversation. */ export const useAgentBuilderAttachment = ({ attachmentType, attachmentData, attachmentPrompt, }: UseAgentBuilderAttachmentParams): UseAgentBuilderAttachmentResult => { - const { http, application, i18n: i18nService, theme, userProfile } = useKibana().services; - const toasts = useAppToasts(); - const { getAppUrl } = useAppUrl(); - const [isLoading, setIsLoading] = useState(false); + const { onechat } = useKibana().services; - const openAgentBuilderFlyout = useCallback(async () => { - if (!http || !application) { + const openAgentBuilderFlyout = useCallback(() => { + if (!onechat?.openConversationFlyout) { return; } - setIsLoading(true); - try { - // TODO: This API call is temporary until the agent builder UI is ready. - // Once the UI is ready, this will open a flyout with the attachment data instead. - const result = await http.post('/api/agent_builder/converse', { - body: JSON.stringify({ - input: attachmentPrompt, - attachments: [ - { - type: attachmentType, - data: attachmentData, - }, - ], - }), - version: '2023-10-31', - }); + // Create a unique ID for the attachment + const attachmentId = `${attachmentType}-${Date.now()}`; - const conversationId = result?.conversation_id; - const conversationUrl = conversationId - ? getAppUrl({ - appId: 'agent_builder', - path: `/conversations/${conversationId}`, - }) - : null; + // Create the UiAttachment object + const attachment: UiAttachment = { + id: attachmentId, + type: attachmentType, + getContent: async () => attachmentData, + }; - const onViewConversationClick = () => { - if (conversationUrl && application) { - application.navigateToUrl(conversationUrl); - } - }; - - const renderContent = i18n.translate( - 'xpack.securitySolution.agentBuilder.attachment.successText', - { - defaultMessage: 'Your attachment has been sent to the agent builder.', - } - ); - - toasts.addSuccess({ - color: 'success', - iconType: 'check', - title: i18n.translate('xpack.securitySolution.agentBuilder.attachment.successTitle', { - defaultMessage: 'Agent builder conversation started', - }), - text: - conversationUrl && i18nService && theme - ? toMountPoint( - React.createElement(AgentBuilderToastSuccessContent, { - content: renderContent, - onViewConversationClick, - }), - { i18n: i18nService, theme, userProfile } - ) - : renderContent, - }); - } catch (error) { - toasts.addError(error, { - title: i18n.translate('xpack.securitySolution.agentBuilder.attachment.errorTitle', { - defaultMessage: 'Failed to start agent builder conversation', - }), - toastMessage: i18n.translate('xpack.securitySolution.agentBuilder.attachment.errorText', { - defaultMessage: 'There was an error sending your attachment to the agent builder.', - }), - }); - } finally { - setIsLoading(false); - } - }, [ - attachmentType, - attachmentData, - attachmentPrompt, - http, - toasts, - getAppUrl, - application, - i18nService, - theme, - userProfile, - ]); + // Open the conversation flyout with attachment and prefilled message + onechat.openConversationFlyout({ + newConversation: true, + initialMessage: attachmentPrompt, + attachments: [attachment], + }); + }, [attachmentType, attachmentData, attachmentPrompt, onechat]); return { openAgentBuilderFlyout, - isLoading, }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/types.ts b/x-pack/solutions/security/plugins/security_solution/public/types.ts index c4324a530cd67..a40c44fe85a3e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/types.ts @@ -65,6 +65,7 @@ import type { AutomaticImportPluginStart } from '@kbn/automatic-import-plugin/pu import type { ProductFeatureKeys } from '@kbn/security-solution-features'; import type { ElasticAssistantSharedStatePublicPluginStart } from '@kbn/elastic-assistant-shared-state-plugin/public'; import type { InferencePublicStart } from '@kbn/inference-plugin/public'; +import type { OnechatPluginStart } from '@kbn/onechat-plugin/public'; import type { ResolverPluginSetup } from './resolver/types'; import type { Inspect } from '../common/search_strategy'; import type { Detections } from './detections'; @@ -164,6 +165,7 @@ export interface StartPlugins { productDocBase: ProductDocBasePluginStart; elasticAssistantSharedState: ElasticAssistantSharedStatePublicPluginStart; inference: InferencePublicStart; + onechat?: OnechatPluginStart; } export interface StartPluginsDependencies extends StartPlugins { From fbd4bd83a0af5bb80ad5335117982ef7a4572467 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Sun, 30 Nov 2025 09:54:51 -0500 Subject: [PATCH 48/96] rm mandatory workflow, include sessionTag --- .../services/attachments/definitions/product_reference.ts | 1 - .../public/agent_builder/hooks/use_agent_builder_attachment.ts | 1 + .../security_solution/server/agent_builder/attachments/alert.ts | 2 +- .../server/agent_builder/attachments/attack_discovery.ts | 2 +- .../server/agent_builder/attachments/entity_risk.ts | 1 - .../server/agent_builder/attachments/query_help.ts | 1 - 6 files changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/product_reference.ts b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/product_reference.ts index d668d74b4de74..3f7ba94acef28 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/product_reference.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/attachments/definitions/product_reference.ts @@ -46,7 +46,6 @@ PRODUCT REFERENCE DATA: {productReferenceData} --- -MANDATORY WORKFLOW: 1. Extract the query or topic from the product reference data above. diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts index 205d0460e2688..d39c09767050b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts @@ -62,6 +62,7 @@ export const useAgentBuilderAttachment = ({ newConversation: true, initialMessage: attachmentPrompt, attachments: [attachment], + sessionTag: 'security', }); }, [attachmentType, attachmentData, attachmentPrompt, onechat]); diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 774bbcdb24ef6..dae6ad68c8b1e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -75,7 +75,7 @@ SECURITY ALERT DATA: {alertData} --- -MANDATORY WORKFLOW - Complete in order: +Complete in order: 1. Extract alert id: kibana.alert.uuid or _id 2. Extract rule name: kibana.alert.rule.name diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts index 224fb9a727950..3419074323a82 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts @@ -70,7 +70,7 @@ ATTACK DISCOVERY DATA: {attackDiscoveryData} --- -MANDATORY WORKFLOW - Complete in order: +Complete in order: 1. Extract entities from the attack discovery: - host.name (extract all host names mentioned) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts index f3633ee04076a..7b8f68f039ce5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts @@ -66,7 +66,6 @@ RISK ENTITY DATA: {riskEntityData} --- -MANDATORY WORKFLOW: 1. Extract the identifierType and identifier from the risk entity data above. diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts index 70cb88ff71274..0d30875d2ea71 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts @@ -63,7 +63,6 @@ QUERY HELP DATA: {queryHelpData} --- -MANDATORY WORKFLOW: 1. Check the queryLanguage from the query help data above. From a922634d7e3fdc8194d30284056afd90fc9f667e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Sun, 30 Nov 2025 10:04:58 -0500 Subject: [PATCH 49/96] entity risk conditional --- .../tools/entity_risk_score_tool.ts | 39 +++++++++++++++++-- .../security_solution/server/plugin.ts | 7 ++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts index 45e147192e54d..9161cc98552e6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts @@ -5,15 +5,16 @@ * 2.0. */ -import type { ElasticsearchClient } from '@kbn/core/server'; +import type { CoreSetup, ElasticsearchClient } from '@kbn/core/server'; import { z } from '@kbn/zod'; import { ToolType, ToolResultType } from '@kbn/onechat-common'; -import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import type { BuiltinToolDefinition, ToolAvailabilityContext } from '@kbn/onechat-server'; import { getToolResultId } from '@kbn/onechat-server/tools'; import { IdentifierType } from '../../../common/api/entity_analytics/common/common.gen'; import { createGetRiskScores } from '../../lib/entity_analytics/risk_score/get_risk_score'; import type { EntityType } from '../../../common/entity_analytics/types'; import { DEFAULT_ALERTS_INDEX, ESSENTIAL_ALERT_FIELDS } from '../../../common/constants'; +import { getRiskIndex } from '../../../common/search_strategy/security_solution/risk_score/common'; import { getSpaceIdFromRequest } from './helpers'; import { securityTool } from './constants'; @@ -64,12 +65,44 @@ const getAlertsById = async ({ }, {}); }; -export const entityRiskScoreTool = (): BuiltinToolDefinition => { +export const entityRiskScoreTool = ( + core: CoreSetup +): BuiltinToolDefinition => { return { id: SECURITY_ENTITY_RISK_SCORE_TOOL_ID, type: ToolType.builtin, description: `Call this tool to get the latest entity risk score and the inputs that contributed to the calculation for a specific entity (host, user, service, or generic). The risk score is sorted by 'kibana.alert.risk_score'. When reporting the risk score value, use the normalized field 'calculated_score_norm' which ranges from 0-100.`, schema: entityRiskScoreSchema, + availability: { + cacheMode: 'space', + handler: async ({ spaceId }: ToolAvailabilityContext) => { + try { + const [coreStart] = await core.getStartServices(); + const esClient = coreStart.elasticsearch.client.asInternalUser; + const riskIndex = getRiskIndex(spaceId, true); + + const indexExists = await esClient.indices.exists({ + index: riskIndex, + }); + + if (indexExists) { + return { status: 'available' }; + } + + return { + status: 'unavailable', + reason: 'Risk score index does not exist for this space', + }; + } catch (error) { + return { + status: 'unavailable', + reason: `Failed to check risk score index availability: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + }; + } + }, + }, handler: async ({ identifierType, identifier }, { request, esClient, logger }) => { const spaceId = getSpaceIdFromRequest(request); const alertsIndexPattern = `${DEFAULT_ALERTS_INDEX}-${spaceId}`; diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 1a74cbffb4f2c..1e6de0448d35e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -238,7 +238,8 @@ export class Plugin implements ISecuritySolutionPlugin { private registerOnechatAttachmentsAndTools( onechat: SecuritySolutionPluginSetupDependencies['onechat'], - config: ConfigType + config: ConfigType, + core: SecuritySolutionPluginCoreSetupDependencies ): void { if (!onechat || !config.experimentalFeatures.agentBuilderEnabled) { return; @@ -251,7 +252,7 @@ export class Plugin implements ISecuritySolutionPlugin { // Register tools try { - onechat.tools.register(entityRiskScoreTool()); + onechat.tools.register(entityRiskScoreTool(core)); onechat.tools.register(attackDiscoverySearchTool()); onechat.tools.register(securityLabsSearchTool()); } catch (error) { @@ -641,7 +642,7 @@ export class Plugin implements ISecuritySolutionPlugin { // Note: This requires onechat to be added as an optional plugin dependency // Note: The alert attachment type may already be registered by onechat's built-in types. // If so, we'll skip registration and use the built-in version. - this.registerOnechatAttachmentsAndTools(plugins.onechat, config); + this.registerOnechatAttachmentsAndTools(plugins.onechat, config, core); return { setProductFeaturesConfigurator: From 3aec4f16dd53ce515f804de5df548c657e3b8186 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Sun, 30 Nov 2025 11:53:24 -0500 Subject: [PATCH 50/96] better --- .../agent_builder/components/prompts.ts | 6 +- .../server/agent_builder/attachments/alert.ts | 6 +- .../agent_builder/attachments/entity_risk.ts | 1 + .../tools/security_labs_search_tool.ts | 64 +++++++++++++++++-- .../security_solution/server/plugin.ts | 2 +- 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts index 04da2828fce08..8292ed325f2eb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts @@ -14,7 +14,7 @@ export const ALERT_ATTACHMENT_PROMPT = `Evaluate the provided security alert and * Summarize the alert using extracted data: - * **Alert ID**: \`kibana.alert.uuid\` or \`_id\` + * **Alert ID**: Use the \`_id\` field value (not \`kibana.alert.uuid\`) * **Rule Name**: \`kibana.alert.rule.name\` * **Entities**: \`host.name\`, \`user.name\`, \`service.name\` * Include associated **risk scores** for each entity (from the risk score tool) @@ -75,9 +75,7 @@ export const ALERT_ATTACHMENT_PROMPT = `Evaluate the provided security alert and * Use markdown headers, tables, and code blocks for clarity * Organize sections visually and consistently * Use concise, actionable language -* Include emojis in section headers for clarity - -**CRITICAL:** You MUST incorporate results from **all enrichment tools** (risk scores, attack discoveries, related cases, Security Labs) before generating the response. Do not skip any step.`; +* Include emojis in section headers for clarity`; export const ENTITY_ANALYSIS = `Analyze asset data described above to provide security insights. The data contains the context of a specific asset (e.g., a host, user, service or cloud resource). Your response must be structured, contextual, and provide a general analysis based on the structure below. Your response must be in markdown format and include the following sections: **1. 🔍 Asset Overview** diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index dae6ad68c8b1e..4f239c9b972e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -81,7 +81,8 @@ Complete in order: 2. Extract rule name: kibana.alert.rule.name 3. Extract entities: host.name, user.name, service.name 4. Extract MITRE fields: kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, threat.tactic.id -5. Query RISK SCORES for entities: +5. IMPORTANT Skip step if ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} is unavailable! + Query RISK SCORES for entities: Tool: ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} Parameters: { identifierType: "host.name", identifier: "MyHostName" } @@ -93,7 +94,8 @@ Complete in order: Tool: ${sanitizeToolId(platformCoreTools.cases)} Parameters: { query: "Do I have any open security cases?", alertIds: ["[alert ID]"], owner: "securitySolution" } -8. Query SECURITY LABS: +8. IMPORTANT Skip step if ${sanitizeToolId(SECURITY_LABS_SEARCH_TOOL_ID)} is unavailable! + Query SECURITY LABS: Tool: ${sanitizeToolId(SECURITY_LABS_SEARCH_TOOL_ID)} Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]" } diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts index 7b8f68f039ce5..bbf34131dc04d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts @@ -74,6 +74,7 @@ RISK ENTITY DATA: Parameters: { identifierType: "[extracted identifierType]", identifier: "[extracted identifier]" } CRITICAL: You MUST call ${sanitizeToolId( + // ^^ we can say MUST here because this attachment is only exposed to the user once risk index has been installed SECURITY_ENTITY_RISK_SCORE_TOOL_ID )} with the extracted identifierType and identifier before responding.`; return description; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts index 8b5e83f364922..bb8f335f2a1ef 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts @@ -5,11 +5,13 @@ * 2.0. */ +import type { CoreSetup } from '@kbn/core/server'; import { z } from '@kbn/zod'; import { ToolType, ToolResultType } from '@kbn/onechat-common'; -import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import type { BuiltinToolDefinition, ToolAvailabilityContext } from '@kbn/onechat-server'; import { runSearchTool } from '@kbn/onechat-genai-utils/tools/search/run_search_tool'; import { SECURITY_LABS_RESOURCE } from '@kbn/elastic-assistant-plugin/server/routes/knowledge_base/constants'; +import { getSpaceIdFromRequest } from './helpers'; import { securityTool } from './constants'; const securityLabsSearchSchema = z.object({ @@ -22,26 +24,74 @@ const securityLabsSearchSchema = z.object({ export const SECURITY_LABS_SEARCH_TOOL_ID = securityTool('security_labs_search'); -const SECURITY_LABS_INDEX_PATTERN = '.kibana-elastic-ai-assistant-knowledge-base-default'; +const getKnowledgeBaseIndex = (spaceId: string): string => + `.kibana-elastic-ai-assistant-knowledge-base-${spaceId}`; -export const securityLabsSearchTool = (): BuiltinToolDefinition< - typeof securityLabsSearchSchema -> => { +export const securityLabsSearchTool = ( + core: CoreSetup +): BuiltinToolDefinition => { return { id: SECURITY_LABS_SEARCH_TOOL_ID, type: ToolType.builtin, description: `Search and analyze Security Labs knowledge base content. Use this tool to find Security Labs articles about specific malware, attack techniques, MITRE ATT&CK techniques, or rule names. Automatically filters to Security Labs content only and limits results to 10 articles.`, schema: securityLabsSearchSchema, - handler: async ({ query: nlQuery }, { esClient, modelProvider, logger, events }) => { + availability: { + cacheMode: 'space', + handler: async ({ spaceId }: ToolAvailabilityContext) => { + try { + const [coreStart] = await core.getStartServices(); + const esClient = coreStart.elasticsearch.client.asInternalUser; + const knowledgeBaseIndex = getKnowledgeBaseIndex(spaceId); + + const response = await esClient.search({ + index: knowledgeBaseIndex, + size: 1, + terminate_after: 1, + query: { + bool: { + filter: [ + { + term: { + kb_resource: SECURITY_LABS_RESOURCE, + }, + }, + ], + }, + }, + }); + console.log('response ==>', JSON.stringify(response, null, 2)); + + if (response.hits.hits.length > 0) { + return { status: 'available' }; + } + + return { + status: 'unavailable', + reason: 'Security Labs content not found in knowledge base', + }; + } catch (error) { + return { + status: 'unavailable', + reason: `Failed to check Security Labs knowledge base availability: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + }; + } + }, + }, + handler: async ({ query: nlQuery }, { request, esClient, modelProvider, logger, events }) => { logger.debug(`${SECURITY_LABS_SEARCH_TOOL_ID} tool called with query: ${nlQuery}`); try { + const spaceId = getSpaceIdFromRequest(request); + const knowledgeBaseIndex = getKnowledgeBaseIndex(spaceId); + // Enhance query to filter by Security Labs resource and limit results const enhancedQuery = `${nlQuery} Filter to only Security Labs content (kb_resource: ${SECURITY_LABS_RESOURCE}). Limit to 3 results.`; const results = await runSearchTool({ nlQuery: enhancedQuery, - index: SECURITY_LABS_INDEX_PATTERN, + index: knowledgeBaseIndex, model: await modelProvider.getDefaultModel(), esClient: esClient.asCurrentUser, logger, diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 1e6de0448d35e..33db315ea4c97 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -254,7 +254,7 @@ export class Plugin implements ISecuritySolutionPlugin { try { onechat.tools.register(entityRiskScoreTool(core)); onechat.tools.register(attackDiscoverySearchTool()); - onechat.tools.register(securityLabsSearchTool()); + onechat.tools.register(securityLabsSearchTool(core)); } catch (error) { this.logger.warn(`Failed to register onechat tools: ${error}`); } From e11d92640ec4191ec0b39472df0438b5e14cf644 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Sun, 30 Nov 2025 12:21:05 -0500 Subject: [PATCH 51/96] fix structure --- .../agent_builder_platform/kibana.jsonc | 2 +- .../attachment_types/product_reference.ts | 72 +++++++++++++++ .../server/tools}/cases/cases.ts | 4 +- .../server/tools}/cases/helpers.ts | 91 ++++++++++++++++++- .../server/tools/index.ts | 2 + .../agent_builder_platform/server/types.ts | 4 + .../plugins/shared/onechat/kibana.jsonc | 1 - .../plugins/shared/onechat/server/types.ts | 2 - 8 files changed, 169 insertions(+), 9 deletions(-) rename x-pack/platform/plugins/shared/{onechat/server/services/tools/builtin/definitions => agent_builder_platform/server/tools}/cases/cases.ts (98%) rename x-pack/platform/plugins/shared/{onechat/server/services/tools/builtin/definitions => agent_builder_platform/server/tools}/cases/helpers.ts (79%) diff --git a/x-pack/platform/plugins/shared/agent_builder_platform/kibana.jsonc b/x-pack/platform/plugins/shared/agent_builder_platform/kibana.jsonc index cffd4b7cbe9fb..41b9d331377e1 100644 --- a/x-pack/platform/plugins/shared/agent_builder_platform/kibana.jsonc +++ b/x-pack/platform/plugins/shared/agent_builder_platform/kibana.jsonc @@ -11,7 +11,7 @@ "configPath": ["xpack", "agentBuilderPlatform"], "requiredPlugins": ["onechat"], "requiredBundles": [], - "optionalPlugins": ["workflowsManagement"], + "optionalPlugins": ["cases", "workflowsManagement"], "extraPublicDirs": [] } } diff --git a/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts b/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts index e69de29bb2d1d..3f7ba94acef28 100644 --- a/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts +++ b/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts @@ -0,0 +1,72 @@ +/* + * 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. + */ + +import type { ProductReferenceAttachmentData } from '@kbn/onechat-common/attachments'; +import { + AttachmentType, + productReferenceAttachmentDataSchema, +} from '@kbn/onechat-common/attachments'; +import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; + +/** + * Creates the definition for the `product_reference` attachment type. + */ +export const createProductReferenceAttachmentType = (): AttachmentTypeDefinition< + AttachmentType.product_reference, + ProductReferenceAttachmentData +> => { + return { + id: AttachmentType.product_reference, + validate: (input) => { + const parseResult = productReferenceAttachmentDataSchema.safeParse(input); + if (parseResult.success) { + return { valid: true, data: parseResult.data }; + } else { + return { valid: false, error: parseResult.error.message }; + } + }, + format: (attachment) => { + return { + getRepresentation: () => { + return { type: 'text', value: formatProductReferenceData(attachment.data) }; + }, + }; + }, + // TODO use real tool once https://github.com/elastic/kibana/pull/242598 merges, same in description below + getTools: () => [`platformCoreTools.productDocumentation`], + getAgentDescription: () => { + const description = `You have access to a product reference that needs to be queried for documentation. + +PRODUCT REFERENCE DATA: +{productReferenceData} + +--- + +1. Extract the query or topic from the product reference data above. + +2. Query PRODUCT DOCUMENTATION for relevant documentation: + Tool: ${sanitizeToolId(`platformCoreTools.productDocumentation`)} + Parameters: { + query: "[extracted query or topic from the product reference]", + product: "[optional: 'kibana' | 'elasticsearch' | 'observability' | 'security']", + max: 3 + }`; + return description; + }, + }; +}; + +/** + * Formats product reference data for display. + * + * @param data - The product reference attachment data containing the text + * @returns Formatted string representation of the product reference data + */ +const formatProductReferenceData = (data: ProductReferenceAttachmentData): string => { + return data.text; +}; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts b/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/cases/cases.ts similarity index 98% rename from x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts rename to x-pack/platform/plugins/shared/agent_builder_platform/server/tools/cases/cases.ts index eb413afe45f53..6b9189c89e2fe 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/cases.ts +++ b/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/cases/cases.ts @@ -14,7 +14,7 @@ import type { CasesSearchRequest } from '@kbn/cases-plugin/common/types/api'; import type { CasesClient } from '@kbn/cases-plugin/server/client'; import type { KibanaRequest } from '@kbn/core-http-server'; import type { Logger } from '@kbn/logging'; -import type { OnechatStartDependencies, OnechatPluginStart } from '../../../../../types'; +import type { AgentBuilderPlatformPluginStart, PluginStartDependencies } from '../../types'; import { normalizeTimeRange, createCommentSummariesFromArray, @@ -223,7 +223,7 @@ function enhanceCases( } export const casesTool = ( - coreSetup: CoreSetup + coreSetup: CoreSetup ): BuiltinToolDefinition => { return { id: platformCoreTools.cases, diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/helpers.ts b/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/cases/helpers.ts similarity index 79% rename from x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/helpers.ts rename to x-pack/platform/plugins/shared/agent_builder_platform/server/tools/cases/helpers.ts index 45b5f98722c0c..6b547f0770c66 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/cases/helpers.ts +++ b/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/cases/helpers.ts @@ -12,6 +12,7 @@ import type { Logger } from '@kbn/logging'; import type { KibanaRequest } from '@kbn/core-http-server'; import type { CasesClient } from '@kbn/cases-plugin/server/client'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; +import { addSpaceIdToPath } from '@kbn/spaces-plugin/server'; import type { Case, Attachment, @@ -19,8 +20,9 @@ import type { UserCommentAttachment, } from '@kbn/cases-plugin/common/types/domain'; import type { CasesSearchRequest } from '@kbn/cases-plugin/common/types/api'; -import type { OnechatStartDependencies } from '../../../../../types'; -import { getCaseUrl } from '../../../../../utils/case_urls'; +import { getCurrentSpaceId } from '@kbn/onechat-plugin/server/utils/spaces'; +import { getCaseViewPath } from '@kbn/cases-plugin/server/common/utils'; +import type { PluginStartDependencies } from '../../types'; export interface CommentSummary { id: string; @@ -268,7 +270,7 @@ export const createErrorResponse = ( * @returns Promise resolving to either a cases client or an error result object */ export const getCasesClient = async ( - pluginsStart: OnechatStartDependencies, + pluginsStart: PluginStartDependencies, request: KibanaRequest, logger: Logger, timeRange: ReturnType | null @@ -305,3 +307,86 @@ export const deduplicateCases = (casesArrays: RelatedCase[][]): RelatedCase[] => } return Array.from(casesMap.values()); }; + +// CASE URL + +/** + * App routes for different Kibana applications + */ +const APP_ROUTES = { + security: '/app/security', + observability: '/app/observability', + management: '/app/management/insightsAndAlerting', +} as const; + +/** + * Get the app route based on owner/case type + */ +function getAppRoute(owner: string): string { + const ownerToRoute: Record = { + securitySolution: APP_ROUTES.security, + observability: APP_ROUTES.observability, + cases: APP_ROUTES.management, + }; + return ownerToRoute[owner] || APP_ROUTES.management; +} + +/** + * Build a full URL from base components + */ +function buildFullUrl( + request: KibanaRequest, + core: CoreStart, + spaceId: string, + path: string +): string { + const publicBaseUrl = core.http.basePath.publicBaseUrl; + const serverBasePath = core.http.basePath.serverBasePath; + + // First try using publicBaseUrl if configured + if (publicBaseUrl) { + const pathWithSpace = addSpaceIdToPath(serverBasePath, spaceId, path); + return `${publicBaseUrl}${pathWithSpace}`; + } + + // Fallback: construct URL from request + const protocol = request.headers['x-forwarded-proto'] || 'http'; + const host = request.headers.host || 'localhost:5601'; + const baseUrl = `${protocol}://${host}`; + const pathWithSpace = addSpaceIdToPath(serverBasePath, spaceId, path); + + return `${baseUrl}${pathWithSpace}`; +} + +/** + * Generate a URL to a case + */ +export function getCaseUrl( + request: KibanaRequest, + core: CoreStart, + spaces: SpacesPluginStart | undefined, + caseId: string, + owner: string +): string | null { + try { + const spaceId = getCurrentSpaceId({ request, spaces }); + const publicBaseUrl = core.http.basePath.publicBaseUrl; + + // getCaseViewPath returns a full URL when publicBaseUrl is provided + if (publicBaseUrl) { + return getCaseViewPath({ + publicBaseUrl, + spaceId, + caseId, + owner, + }); + } + + // Fallback: construct URL manually + const appRoute = getAppRoute(owner); + const path = `${appRoute}/cases/${caseId}`; + return buildFullUrl(request, core, spaceId, path); + } catch (error) { + return null; + } +} diff --git a/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/index.ts b/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/index.ts index 7443bc8dc77b7..ae81539f41857 100644 --- a/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/index.ts +++ b/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/index.ts @@ -12,6 +12,7 @@ import type { PluginSetupDependencies, PluginStartDependencies, } from '../types'; +import { casesTool } from './cases/cases'; import { getDocumentByIdTool } from './get_document_by_id'; import { getIndexMappingsTool } from './get_index_mapping'; import { listIndicesTool } from './list_indices'; @@ -39,6 +40,7 @@ export const registerTools = ({ listIndicesTool(), indexExplorerTool(), createVisualizationTool(), + casesTool(coreSetup), ]; tools.forEach((tool) => { diff --git a/x-pack/platform/plugins/shared/agent_builder_platform/server/types.ts b/x-pack/platform/plugins/shared/agent_builder_platform/server/types.ts index 21c5b93d7d870..582f726e4a4f3 100644 --- a/x-pack/platform/plugins/shared/agent_builder_platform/server/types.ts +++ b/x-pack/platform/plugins/shared/agent_builder_platform/server/types.ts @@ -7,6 +7,8 @@ import type { WorkflowsServerPluginSetup } from '@kbn/workflows-management-plugin/server'; import type { OnechatPluginSetup, OnechatPluginStart } from '@kbn/onechat-plugin/server'; +import type { CasesServerStart } from '@kbn/cases-plugin/server'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; export interface PluginSetupDependencies { workflowsManagement?: WorkflowsServerPluginSetup; @@ -15,6 +17,8 @@ export interface PluginSetupDependencies { export interface PluginStartDependencies { onechat: OnechatPluginStart; + cases?: CasesServerStart; + spaces?: SpacesPluginStart; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/x-pack/platform/plugins/shared/onechat/kibana.jsonc b/x-pack/platform/plugins/shared/onechat/kibana.jsonc index 50c4d42031362..4d533558a21f9 100644 --- a/x-pack/platform/plugins/shared/onechat/kibana.jsonc +++ b/x-pack/platform/plugins/shared/onechat/kibana.jsonc @@ -34,7 +34,6 @@ "kibanaReact" ], "optionalPlugins": [ - "cases", "cloud", "licenseManagement", "workflowsManagement", diff --git a/x-pack/platform/plugins/shared/onechat/server/types.ts b/x-pack/platform/plugins/shared/onechat/server/types.ts index 55229579b402a..0f8ad0210a816 100644 --- a/x-pack/platform/plugins/shared/onechat/server/types.ts +++ b/x-pack/platform/plugins/shared/onechat/server/types.ts @@ -17,7 +17,6 @@ import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import type { BuiltInAgentDefinition } from '@kbn/onechat-server/agents'; import type { ToolsServiceSetup, ToolRegistry } from './services/tools'; import type { AttachmentServiceSetup } from './services/attachments'; -import type { CasesServerStart } from '@kbn/cases-plugin/server'; export interface OnechatSetupDependencies { cloud?: CloudSetup; @@ -33,7 +32,6 @@ export interface OnechatStartDependencies { licensing: LicensingPluginStart; cloud?: CloudStart; spaces?: SpacesPluginStart; - cases?: CasesServerStart; } export interface AttachmentsSetup { From 75adb9c9f8201c6aee09f16167f28eede71230f6 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Sun, 30 Nov 2025 12:50:03 -0500 Subject: [PATCH 52/96] registration cleanup --- .../attachments/register_attachments.ts | 22 ++++++++++ .../agent_builder/tools/register_tools.ts | 41 +++++++++++++++++++ .../security_solution/server/plugin.ts | 32 ++------------- 3 files changed, 67 insertions(+), 28 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts new file mode 100644 index 0000000000000..4dd72308cdffc --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +import type { OnechatPluginSetup } from '@kbn/onechat-plugin/server'; +import { createAlertAttachmentType } from './alert'; +import { createAttackDiscoveryAttachmentType } from './attack_discovery'; +import { createEntityRiskAttachmentType } from './entity_risk'; +import { createQueryHelpAttachmentType } from './query_help'; + +/** + * Registers all security agent builder attachments with the onechat plugin + */ +export const registerAttachments = (onechat: OnechatPluginSetup): void => { + onechat.attachments.registerType(createAlertAttachmentType()); + onechat.attachments.registerType(createAttackDiscoveryAttachmentType()); + onechat.attachments.registerType(createEntityRiskAttachmentType()); + onechat.attachments.registerType(createQueryHelpAttachmentType()); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts new file mode 100644 index 0000000000000..13106ab70bddb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts @@ -0,0 +1,41 @@ +/* + * 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. + */ + +import { platformCoreTools } from '@kbn/onechat-common'; +import type { OnechatPluginSetup } from '@kbn/onechat-plugin/server'; +import type { CoreSetup } from '@kbn/core-lifecycle-server'; +import { SECURITY_LABS_SEARCH_TOOL_ID, securityLabsSearchTool } from './security_labs_search_tool'; +import { + attackDiscoverySearchTool, + SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, +} from './attack_discovery_search_tool'; +import { entityRiskScoreTool, SECURITY_ENTITY_RISK_SCORE_TOOL_ID } from './entity_risk_score_tool'; + +const PLATFORM_TOOL_IDS = [ + platformCoreTools.search, + platformCoreTools.listIndices, + platformCoreTools.getIndexMapping, + platformCoreTools.getDocumentById, + // TODO add once product doc tool is merged https://github.com/elastic/kibana/pull/242598 + // platformCoreTools.productDocumentation, +]; +export const SECURITY_TOOL_IDS = [ + SECURITY_LABS_SEARCH_TOOL_ID, + SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, + SECURITY_ENTITY_RISK_SCORE_TOOL_ID, +]; + +export const SECURITY_AGENT_TOOL_IDS = [...PLATFORM_TOOL_IDS, ...SECURITY_TOOL_IDS]; + +/** + * Registers all security agent builder tools with the onechat plugin + */ +export const registerTools = (onechat: OnechatPluginSetup, core: CoreSetup): void => { + onechat.tools.register(entityRiskScoreTool(core)); + onechat.tools.register(attackDiscoverySearchTool()); + onechat.tools.register(securityLabsSearchTool(core)); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 33db315ea4c97..65f3e45791e25 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -19,6 +19,8 @@ import type { ILicense } from '@kbn/licensing-types'; import type { NewPackagePolicy, UpdatePackagePolicy } from '@kbn/fleet-plugin/common'; import { FLEET_ENDPOINT_PACKAGE } from '@kbn/fleet-plugin/common'; +import { registerAttachments } from './agent_builder/attachments/register_attachments'; +import { registerTools } from './agent_builder/tools/register_tools'; import { migrateEndpointDataToSupportSpaces } from './endpoint/migrations/space_awareness_migration'; import { SavedObjectsClientFactory } from './endpoint/services/saved_objects'; import { registerEntityStoreDataViewRefreshTask } from './lib/entity_analytics/entity_store/tasks/data_view_refresh/data_view_refresh_task'; @@ -146,17 +148,6 @@ import { HealthDiagnosticServiceImpl } from './lib/telemetry/diagnostic/health_d import type { HealthDiagnosticService } from './lib/telemetry/diagnostic/health_diagnostic_service.types'; import { ENTITY_RISK_SCORE_TOOL_ID } from './assistant/tools/entity_risk_score/entity_risk_score'; import type { TelemetryQueryConfiguration } from './lib/telemetry/types'; -import { - createAlertAttachmentType, - createAttackDiscoveryAttachmentType, - createEntityRiskAttachmentType, - createQueryHelpAttachmentType, -} from './agent_builder/attachments'; -import { - entityRiskScoreTool, - attackDiscoverySearchTool, - securityLabsSearchTool, -} from './agent_builder/tools'; export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract'; @@ -245,19 +236,8 @@ export class Plugin implements ISecuritySolutionPlugin { return; } - onechat.attachments.registerType(createAlertAttachmentType()); - onechat.attachments.registerType(createAttackDiscoveryAttachmentType()); - onechat.attachments.registerType(createEntityRiskAttachmentType()); - onechat.attachments.registerType(createQueryHelpAttachmentType()); - - // Register tools - try { - onechat.tools.register(entityRiskScoreTool(core)); - onechat.tools.register(attackDiscoverySearchTool()); - onechat.tools.register(securityLabsSearchTool(core)); - } catch (error) { - this.logger.warn(`Failed to register onechat tools: ${error}`); - } + registerTools(onechat, core); + registerAttachments(onechat); } public setup( @@ -638,10 +618,6 @@ export class Plugin implements ISecuritySolutionPlugin { this.logger.warn('Task Manager not available, health diagnostic task not registered.'); } - // Register alert attachment type and alerts tool with onechat - // Note: This requires onechat to be added as an optional plugin dependency - // Note: The alert attachment type may already be registered by onechat's built-in types. - // If so, we'll skip registration and use the built-in version. this.registerOnechatAttachmentsAndTools(plugins.onechat, config, core); return { From 19435a722b6ed8f4849b2faa8937da92de9666f3 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Sun, 30 Nov 2025 13:03:49 -0500 Subject: [PATCH 53/96] make security agent --- .../agent_builder/attachments/register_attachments.ts | 2 +- .../server/agent_builder/tools/register_tools.ts | 2 +- .../security/plugins/security_solution/server/plugin.ts | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts index 4dd72308cdffc..ae38bcd207c9d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts @@ -14,7 +14,7 @@ import { createQueryHelpAttachmentType } from './query_help'; /** * Registers all security agent builder attachments with the onechat plugin */ -export const registerAttachments = (onechat: OnechatPluginSetup): void => { +export const registerAttachments = async (onechat: OnechatPluginSetup) => { onechat.attachments.registerType(createAlertAttachmentType()); onechat.attachments.registerType(createAttackDiscoveryAttachmentType()); onechat.attachments.registerType(createEntityRiskAttachmentType()); diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts index 13106ab70bddb..206e281640220 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts @@ -34,7 +34,7 @@ export const SECURITY_AGENT_TOOL_IDS = [...PLATFORM_TOOL_IDS, ...SECURITY_TOOL_I /** * Registers all security agent builder tools with the onechat plugin */ -export const registerTools = (onechat: OnechatPluginSetup, core: CoreSetup): void => { +export const registerTools = async (onechat: OnechatPluginSetup, core: CoreSetup) => { onechat.tools.register(entityRiskScoreTool(core)); onechat.tools.register(attackDiscoverySearchTool()); onechat.tools.register(securityLabsSearchTool(core)); diff --git a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts index 65f3e45791e25..dea5d43b12456 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/plugin.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/plugin.ts @@ -236,8 +236,12 @@ export class Plugin implements ISecuritySolutionPlugin { return; } - registerTools(onechat, core); - registerAttachments(onechat); + registerTools(onechat, core).catch((error) => { + this.logger.error(`Error registering security tools: ${error}`); + }); + registerAttachments(onechat).catch((error) => { + this.logger.error(`Error registering security attachments: ${error}`); + }); } public setup( From ee63b8be99892c232b749fb6418d73e2c4370997 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 1 Dec 2025 13:31:50 +0100 Subject: [PATCH 54/96] one alert attachment --- .../security_solution/common/constants.ts | 1 - .../actionable_summary/index.tsx | 4 +- .../tabs/attack_discovery_tab/index.tsx | 4 +- .../pages/results/take_action/index.tsx | 4 +- .../server/agent_builder/attachments/alert.ts | 33 +---- .../attachments/attack_discovery.ts | 115 ------------------ .../server/agent_builder/attachments/index.ts | 1 - .../attachments/register_attachments.ts | 2 - 8 files changed, 8 insertions(+), 156 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index 90e9df28acfe9..9b8d85b243aab 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -713,7 +713,6 @@ export const ESSENTIAL_ALERT_FIELDS: string[] = [ ] as const; export enum SecurityAgentBuilderAttachments { - attack_discovery = 'security.attack_discovery', alert = 'security.alert', risk_entity = 'security.risk_entity', query_help = 'security.query_help', diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx index f9c09eef1133f..8a0d6fbc9f1df 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actionable_summary/index.tsx @@ -86,8 +86,8 @@ const ActionableSummaryComponent: React.FC = ({ ); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: SecurityAgentBuilderAttachments.attack_discovery, - attachmentData: { attackDiscovery: attackDiscoveryWithOriginalValues }, + attachmentType: SecurityAgentBuilderAttachments.alert, + attachmentData: { alert: attackDiscoveryWithOriginalValues }, attachmentPrompt: ATTACK_DISCOVERY_ATTACHMENT_PROMPT, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx index 429f586ab2a08..0b1e173d15910 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/tabs/attack_discovery_tab/index.tsx @@ -97,8 +97,8 @@ const AttackDiscoveryTabComponent: React.FC = ({ ); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: SecurityAgentBuilderAttachments.attack_discovery, - attachmentData: { attackDiscovery: attackDiscoveryWithOriginalValues }, + attachmentType: SecurityAgentBuilderAttachments.alert, + attachmentData: { alert: attackDiscoveryWithOriginalValues }, attachmentPrompt: ATTACK_DISCOVERY_ATTACHMENT_PROMPT, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx index 846216abc124d..e158aca9b1573 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/results/take_action/index.tsx @@ -211,9 +211,9 @@ const TakeActionComponent: React.FC = ({ const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); const attackDiscovery = attackDiscoveries.length === 1 ? attackDiscoveries[0] : null; const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: SecurityAgentBuilderAttachments.attack_discovery, + attachmentType: SecurityAgentBuilderAttachments.alert, attachmentData: { - attackDiscovery: attackDiscovery + alert: attackDiscovery ? getAttackDiscoveryMarkdown({ attackDiscovery, replacements, diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 4f239c9b972e9..cd260c10c35f6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -6,7 +6,6 @@ */ import { z } from '@kbn/zod'; -import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; import type { Attachment } from '@kbn/onechat-common/attachments'; import { platformCoreTools } from '@kbn/onechat-common'; @@ -77,39 +76,11 @@ SECURITY ALERT DATA: --- Complete in order: -1. Extract alert id: kibana.alert.uuid or _id +1. Extract alert id(s): _id 2. Extract rule name: kibana.alert.rule.name 3. Extract entities: host.name, user.name, service.name 4. Extract MITRE fields: kibana.alert.rule.threat.tactic.id, kibana.alert.rule.threat.technique.id, threat.tactic.id -5. IMPORTANT Skip step if ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} is unavailable! - Query RISK SCORES for entities: - Tool: ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} - Parameters: { identifierType: "host.name", identifier: "MyHostName" } - -6. Query ATTACK DISCOVERIES for the extracted alert id: - Tool: ${sanitizeToolId(SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID)} - Parameters: { alertIds: ["[alert ID]"] } - -7. Query CASES with the extracted alert id to find related cases. Case URLs must be included in response. - Tool: ${sanitizeToolId(platformCoreTools.cases)} - Parameters: { query: "Do I have any open security cases?", alertIds: ["[alert ID]"], owner: "securitySolution" } - -8. IMPORTANT Skip step if ${sanitizeToolId(SECURITY_LABS_SEARCH_TOOL_ID)} is unavailable! - Query SECURITY LABS: - Tool: ${sanitizeToolId(SECURITY_LABS_SEARCH_TOOL_ID)} - Parameters: { query: "Find Security Labs articles about [MITRE technique or rule name]" } - -9. Generate ESQL for related entities: - Tool: ${sanitizeToolId(platformCoreTools.generateEsql)} - Parameters: { query: "Write ESQL query to find events in the security solution data view from host.name: "MyHostName" } - -CRITICAL: You MUST call all 5 tools (steps 5-9) before responding. Do not skip any step.`; - // TODO add this once product doc tool available - // 9. Query PRODUCT DOCUMENTATION: - // Tool: ${sanitizeToolId(platformCoreTools.productDocumentation)} - // Parameters: { query: "Find alert triage steps", product: "security" } - // - // CRITICAL: You MUST call all 6 tools (steps 5-10) before responding. Do not skip any step.`; +5. Use the available tools to gather context about the alert and provide a response.`; return description; }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts deleted file mode 100644 index 3419074323a82..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/attack_discovery.ts +++ /dev/null @@ -1,115 +0,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 Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod'; -import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; -import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; -import type { Attachment } from '@kbn/onechat-common/attachments'; -import { platformCoreTools } from '@kbn/onechat-common'; -import { SecurityAgentBuilderAttachments } from '../../../common/constants'; -import { SECURITY_ENTITY_RISK_SCORE_TOOL_ID, SECURITY_LABS_SEARCH_TOOL_ID } from '../tools'; - -export const attackDiscoveryAttachmentDataSchema = z.object({ - attackDiscovery: z.string(), -}); - -/** - * Data for an attack discovery attachment. - */ -export type AttackDiscoveryAttachmentData = z.infer; - -/** - * Type guard to narrow attachment data to AttackDiscoveryAttachmentData - */ -const isAttackDiscoveryAttachmentData = (data: unknown): data is AttackDiscoveryAttachmentData => { - return attackDiscoveryAttachmentDataSchema.safeParse(data).success; -}; - -/** - * Creates the definition for the `attack_discovery` attachment type. - */ -export const createAttackDiscoveryAttachmentType = (): AttachmentTypeDefinition => { - return { - id: SecurityAgentBuilderAttachments.attack_discovery, - validate: (input) => { - const parseResult = attackDiscoveryAttachmentDataSchema.safeParse(input); - if (parseResult.success) { - return { valid: true, data: parseResult.data }; - } else { - return { valid: false, error: parseResult.error.message }; - } - }, - format: (attachment: Attachment) => { - // Extract data to allow proper type narrowing - const data = attachment.data; - - // Necessary because we cannot currently use AttachmentType type as agent is not - // registered with enum AttachmentType in onechat attachment_types.ts - if (!isAttackDiscoveryAttachmentData(data)) { - throw new Error(`Invalid attack discovery attachment data for attachment ${attachment.id}`); - } - return { - getRepresentation: () => { - return { type: 'text', value: formatAttackDiscoveryData(data) }; - }, - }; - }, - getTools: () => [ - SECURITY_ENTITY_RISK_SCORE_TOOL_ID, - SECURITY_LABS_SEARCH_TOOL_ID, - platformCoreTools.cases, - ], - getAgentDescription: () => { - const description = `You have access to security attack discovery data. To provide a comprehensive analysis, you MUST gather enriched context by querying for related information. - -ATTACK DISCOVERY DATA: -{attackDiscoveryData} - ---- -Complete in order: - -1. Extract entities from the attack discovery: - - host.name (extract all host names mentioned) - - user.name (extract all user names mentioned) - - service.name (extract all service names mentioned) - -2. Extract MITRE tactics/techniques from the attack discovery: - - Look for MITRE ATT&CK tactics in the "Attack Chain" section - - Extract tactic IDs and technique IDs if mentioned - -3. Extract alert IDs from the attack discovery: - - Look for alert IDs referenced in the attack discovery - - These may be mentioned in the details or summary sections - -4. Query ENTITY RISK SCORE for entities: - Tool: ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} - Parameters: { identifierType: "host.name", identifier: "[host name]" } - Repeat for each unique host.name, user.name found - -5. Query SECURITY LABS for MITRE tactics/techniques: - Tool: ${sanitizeToolId(SECURITY_LABS_SEARCH_TOOL_ID)} - Parameters: { query: "Find Security Labs articles about [MITRE tactic or technique]" } - -6. Query CASES with alert IDs from the attack discovery to find related cases. Case URLs must be included in response. - Tool: ${sanitizeToolId(platformCoreTools.cases)} - Parameters: { query: "Do I have any open security cases?", alertIds: ["[alert ID]"], owner: "securitySolution" } - -CRITICAL: You MUST call all 3 tools (steps 4-6) before responding. Do not skip any step.`; - return description; - }, - }; -}; - -/** - * Formats attack discovery data for display. - * - * @param data - The attack discovery attachment data containing the attack discovery string - * @returns Formatted string representation of the attack discovery data - */ -const formatAttackDiscoveryData = (data: AttackDiscoveryAttachmentData): string => { - return data.attackDiscovery; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts index 7c4a9731e44e7..6644958f09171 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts @@ -6,6 +6,5 @@ */ export { createAlertAttachmentType } from './alert'; -export { createAttackDiscoveryAttachmentType } from './attack_discovery'; export { createEntityRiskAttachmentType } from './entity_risk'; export { createQueryHelpAttachmentType } from './query_help'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts index ae38bcd207c9d..fc09e9b2134f9 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts @@ -7,7 +7,6 @@ import type { OnechatPluginSetup } from '@kbn/onechat-plugin/server'; import { createAlertAttachmentType } from './alert'; -import { createAttackDiscoveryAttachmentType } from './attack_discovery'; import { createEntityRiskAttachmentType } from './entity_risk'; import { createQueryHelpAttachmentType } from './query_help'; @@ -16,7 +15,6 @@ import { createQueryHelpAttachmentType } from './query_help'; */ export const registerAttachments = async (onechat: OnechatPluginSetup) => { onechat.attachments.registerType(createAlertAttachmentType()); - onechat.attachments.registerType(createAttackDiscoveryAttachmentType()); onechat.attachments.registerType(createEntityRiskAttachmentType()); onechat.attachments.registerType(createQueryHelpAttachmentType()); }; From 8cb1c6e30eec5258aaa3c7ac8340e8cf972b4a84 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 1 Dec 2025 13:53:06 +0100 Subject: [PATCH 55/96] simplify attachment descriptions --- .../attachment_types/product_reference.ts | 12 ++---------- .../agent_builder/attachments/entity_risk.ts | 13 ++----------- .../agent_builder/attachments/query_help.ts | 19 ++----------------- 3 files changed, 6 insertions(+), 38 deletions(-) diff --git a/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts b/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts index 3f7ba94acef28..56433a7d59660 100644 --- a/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts +++ b/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts @@ -11,7 +11,6 @@ import { productReferenceAttachmentDataSchema, } from '@kbn/onechat-common/attachments'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; -import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; /** * Creates the definition for the `product_reference` attachment type. @@ -47,15 +46,8 @@ PRODUCT REFERENCE DATA: --- -1. Extract the query or topic from the product reference data above. - -2. Query PRODUCT DOCUMENTATION for relevant documentation: - Tool: ${sanitizeToolId(`platformCoreTools.productDocumentation`)} - Parameters: { - query: "[extracted query or topic from the product reference]", - product: "[optional: 'kibana' | 'elasticsearch' | 'observability' | 'security']", - max: 3 - }`; +1. Extract the query or topic from the product reference attachment. +2. Use the appropriate tools to provide a response`; return description; }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts index bbf34131dc04d..7cabb6af9430f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts @@ -5,7 +5,6 @@ * 2.0. */ import { z } from '@kbn/zod'; -import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; import type { Attachment } from '@kbn/onechat-common/attachments'; import { SecurityAgentBuilderAttachments } from '../../../common/constants'; @@ -67,16 +66,8 @@ RISK ENTITY DATA: --- -1. Extract the identifierType and identifier from the risk entity data above. - -2. Query ENTITY RISK SCORE for the entity: - Tool: ${sanitizeToolId(SECURITY_ENTITY_RISK_SCORE_TOOL_ID)} - Parameters: { identifierType: "[extracted identifierType]", identifier: "[extracted identifier]" } - -CRITICAL: You MUST call ${sanitizeToolId( - // ^^ we can say MUST here because this attachment is only exposed to the user once risk index has been installed - SECURITY_ENTITY_RISK_SCORE_TOOL_ID - )} with the extracted identifierType and identifier before responding.`; +1. Extract the identifierType and identifier from the provided risk entity attachment. +2. Use the available tools to gather context about the alert and provide a response.`; return description; }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts index 0d30875d2ea71..234bf6c902329 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts @@ -6,7 +6,6 @@ */ import { z } from '@kbn/zod'; -import { sanitizeToolId } from '@kbn/onechat-genai-utils/langchain'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; import type { Attachment } from '@kbn/onechat-common/attachments'; import { platformCoreTools } from '@kbn/onechat-common'; @@ -64,22 +63,8 @@ QUERY HELP DATA: --- -1. Check the queryLanguage from the query help data above. - -2. If queryLanguage is 'esql', use the generateEsql tool to generate a new working ESQL query: - Tool: ${sanitizeToolId(platformCoreTools.generateEsql)} - Parameters: { query: "Write ESQL query to [describe what the broken query was trying to do]" } - -3. Query PRODUCT DOCUMENTATION for relevant documentation when needed: - // TODO use real tool once product_documentation tool is merged - Tool: ${sanitizeToolId('platformCoreTools.productDocumentation')} - Parameters: { - query: "[query about KQL query language, Elasticsearch Query DSL, Lucene]", - product: "[optional: 'kibana' | 'elasticsearch' | 'observability' | 'security']", - max: 3 - } - -CRITICAL: Only use the generateEsql tool if queryLanguage is 'esql'. Otherwise, use productDocumentationTool to find relevant documentation to help fix the query.`; +1. Check the queryLanguage from the query attachment provided. +2. Use the appropriate tools to generate a query.`; return description; }, }; From 86bdf418c38ebc618bdff3af4efb78c018ea7cce Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 1 Dec 2025 14:17:07 +0100 Subject: [PATCH 56/96] tweak --- .../server/agent_builder/attachments/query_help.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts index 234bf6c902329..7c3b5009c5d33 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts @@ -64,7 +64,7 @@ QUERY HELP DATA: --- 1. Check the queryLanguage from the query attachment provided. -2. Use the appropriate tools to generate a query.`; +2. Use the appropriate tools to provide a corrected query.`; return description; }, }; From 89170f3f88700ac421e11d369bd6eb6f6cf7c88a Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 10:33:35 +0100 Subject: [PATCH 57/96] fixes --- .../public/agent_builder/components/prompts.ts | 2 +- .../security_solution/server/agent_builder/attachments/alert.ts | 2 ++ .../server/agent_builder/tools/security_labs_search_tool.ts | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts index 8292ed325f2eb..fa6fc1ddd3f5f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts @@ -5,7 +5,7 @@ * 2.0. */ // temporary until we figure out if we are using prompt system or not -export const ATTACK_DISCOVERY_ATTACHMENT_PROMPT = `Summarize the attack discovery attached and recommend next steps. Case URLs MUST be included in the response if they exist. Summary should be in markdown.`; +export const ATTACK_DISCOVERY_ATTACHMENT_PROMPT = `Summarize the attack discovery attached and recommend next steps. Find the risk score for each extracted host.name and user.name. Case URLs MUST be included in the response if they exist. Summary should be in markdown.`; export const ALERT_ATTACHMENT_PROMPT = `Evaluate the provided security alert and generate a structured, markdown-formatted summary suitable for inclusion in an Elastic Security case. Use all available enrichment tools before generating your response. Include the following sections: --- diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index cd260c10c35f6..688c1b100acec 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -9,6 +9,7 @@ import { z } from '@kbn/zod'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; import type { Attachment } from '@kbn/onechat-common/attachments'; import { platformCoreTools } from '@kbn/onechat-common'; +import { SECURITY_ALERTS_TOOL_ID } from '../tools/alerts_tool'; import { SecurityAgentBuilderAttachments } from '../../../common/constants'; import { SECURITY_ENTITY_RISK_SCORE_TOOL_ID, @@ -64,6 +65,7 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition => { SECURITY_ENTITY_RISK_SCORE_TOOL_ID, SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, SECURITY_LABS_SEARCH_TOOL_ID, + SECURITY_ALERTS_TOOL_ID, platformCoreTools.cases, platformCoreTools.generateEsql, ], diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts index bb8f335f2a1ef..42821757c6ce0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.ts @@ -59,7 +59,6 @@ export const securityLabsSearchTool = ( }, }, }); - console.log('response ==>', JSON.stringify(response, null, 2)); if (response.hits.hits.length > 0) { return { status: 'available' }; From 8e667c09750b19484bc44b9644d90f3423d5de7a Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 10:46:21 +0100 Subject: [PATCH 58/96] move fn --- .../public/agent_builder/helpers.tsx | 23 +++++++++++++++++++ .../public/assistant/helpers.tsx | 16 ------------- .../flyout/document_details/right/footer.tsx | 3 ++- .../public/flyout/ease/footer.tsx | 4 +++- 4 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.tsx new file mode 100644 index 0000000000000..432c3ac6fb81b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.tsx @@ -0,0 +1,23 @@ +/* + * 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. + */ + +import { ESSENTIAL_ALERT_FIELDS } from '../../common'; + +/** + * Filters raw alert data to only include essential fields and stringifies the result. + * This reduces context window usage by keeping only the most relevant information. + */ +export const filterAndStringifyAlertData = (rawData: Record): string => { + const filteredData = ESSENTIAL_ALERT_FIELDS.reduce((acc, key) => { + if (key in rawData) { + acc[key] = rawData[key]; + } + return acc; + }, {} as Record); + + return JSON.stringify(filteredData); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx index c0f1747e094ea..4267dc38eeedc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/helpers.tsx @@ -8,7 +8,6 @@ import type { CodeBlockDetails } from '@kbn/elastic-assistant'; import type { TimelineEventsDetailsItem } from '../../common/search_strategy'; import type { Rule } from '../detection_engine/rule_management/logic'; -import { ESSENTIAL_ALERT_FIELDS } from '../../common/constants'; export const LOCAL_STORAGE_KEY = `securityAssistant`; @@ -23,21 +22,6 @@ export const getRawData = (data: TimelineEventsDetailsItem[]): Record !field.startsWith('signal.')) .reduce((acc, { field, values }) => ({ ...acc, [field]: values ?? [] }), {}); -/** - * Filters raw alert data to only include essential fields and stringifies the result. - * This reduces context window usage by keeping only the most relevant information. - */ -export const filterAndStringifyAlertData = (rawData: Record): string => { - const filteredData = ESSENTIAL_ALERT_FIELDS.reduce((acc, key) => { - if (key in rawData) { - acc[key] = rawData[key]; - } - return acc; - }, {} as Record); - - return JSON.stringify(filteredData); -}; - export const sendToTimelineEligibleQueryTypes: Array = [ 'kql', 'dsl', diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx index 81949736470a0..bfcfe5c3f0992 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx @@ -19,7 +19,8 @@ import { TakeActionButton } from '../shared/components/take_action_button'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { NewAgentBuilderAttachment } from '../../../agent_builder/components/new_agent_builder_attachment'; import { useAgentBuilderAttachment } from '../../../agent_builder/hooks/use_agent_builder_attachment'; -import { getRawData, filterAndStringifyAlertData } from '../../../assistant/helpers'; +import { getRawData } from '../../../assistant/helpers'; +import { filterAndStringifyAlertData } from '../../../agent_builder/helpers'; import { SecurityAgentBuilderAttachments } from '../../../../common/constants'; export const ASK_AI_ASSISTANT = i18n.translate( diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx index dbccfa709adc1..a7a5ba4120955 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx @@ -16,7 +16,8 @@ import { useAssistant } from '../document_details/right/hooks/use_assistant'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { NewAgentBuilderAttachment } from '../../agent_builder/components/new_agent_builder_attachment'; import { useAgentBuilderAttachment } from '../../agent_builder/hooks/use_agent_builder_attachment'; -import { getRawData, filterAndStringifyAlertData } from '../../assistant/helpers'; +import { getRawData } from '../../assistant/helpers'; +import { filterAndStringifyAlertData } from '../../agent_builder/helpers'; import { SecurityAgentBuilderAttachments } from '../../../common/constants'; import { ALERT_ATTACHMENT_PROMPT } from '../../agent_builder/components/prompts'; @@ -39,6 +40,7 @@ export const PanelFooter = memo(() => { dataFormattedForFieldBrowser, isAlert, }); + // TODO enable agent builder for EASE roles const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); const alertData = useMemo(() => { From cb5730d172423be489d9256d2e34c8ceccab16a2 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 10:53:24 +0100 Subject: [PATCH 59/96] risk_entity => entity --- .../plugins/security_solution/common/constants.ts | 2 +- .../components/entity_highlights_settings.tsx | 2 +- .../tabs/risk_inputs/ask_ai_assistant.tsx | 2 +- .../public/flyout/entity_details/generic_right/footer.tsx | 8 +++++--- .../server/agent_builder/attachments/alert.ts | 2 -- .../attachments/{entity_risk.ts => entity.ts} | 6 +++--- .../server/agent_builder/attachments/index.ts | 2 +- .../agent_builder/attachments/register_attachments.ts | 4 ++-- 8 files changed, 14 insertions(+), 14 deletions(-) rename x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/{entity_risk.ts => entity.ts} (93%) diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index 9b8d85b243aab..72190d6fd68ef 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -714,6 +714,6 @@ export const ESSENTIAL_ALERT_FIELDS: string[] = [ export enum SecurityAgentBuilderAttachments { alert = 'security.alert', - risk_entity = 'security.risk_entity', + entity = 'security.entity', query_help = 'security.query_help', } diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx index cbac00bc956cc..27900820998f1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/entity_highlights_settings.tsx @@ -100,7 +100,7 @@ export const EntityHighlightsSettings: React.FC = const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: SecurityAgentBuilderAttachments.risk_entity, + attachmentType: SecurityAgentBuilderAttachments.entity, attachmentData: { identifierType: entityType, identifier: entityIdentifier }, attachmentPrompt: `Investigate the entity and suggest next steps.`, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx index 8c6102cb7fb4f..cdd5203f23432 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/ask_ai_assistant.tsx @@ -67,7 +67,7 @@ export const AskAiAssistant = ({ }); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: SecurityAgentBuilderAttachments.risk_entity, + attachmentType: SecurityAgentBuilderAttachments.entity, attachmentData: { identifierType: entityType, identifier: entityName }, attachmentPrompt: `Explain how inputs contributed to the risk score. Additionally, outline the recommended next steps for investigating or mitigating the risk if the entity is deemed risky.\nTo answer risk score questions, fetch the risk score information and take into consideration the risk score inputs.`, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx index f00c01e82010b..26a8fcadefd39 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx @@ -58,14 +58,16 @@ export const GenericEntityFlyoutFooter = ({ const attachmentData = useMemo(() => { return { - ...entityFields, - 'asset.criticality': assetCriticalityLevel ? [assetCriticalityLevel] : undefined, + text: { + ...entityFields, + 'asset.criticality': assetCriticalityLevel ? [assetCriticalityLevel] : undefined, + }, }; }, [entityFields, assetCriticalityLevel]); // TODO confirm behavior with @maxcold https://github.com/elastic/kibana/pull/234324 const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: AttachmentType.text, + attachmentType: AttachmentType.product_reference, attachmentData, attachmentPrompt: ENTITY_ANALYSIS, }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts index 688c1b100acec..cd260c10c35f6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.ts @@ -9,7 +9,6 @@ import { z } from '@kbn/zod'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; import type { Attachment } from '@kbn/onechat-common/attachments'; import { platformCoreTools } from '@kbn/onechat-common'; -import { SECURITY_ALERTS_TOOL_ID } from '../tools/alerts_tool'; import { SecurityAgentBuilderAttachments } from '../../../common/constants'; import { SECURITY_ENTITY_RISK_SCORE_TOOL_ID, @@ -65,7 +64,6 @@ export const createAlertAttachmentType = (): AttachmentTypeDefinition => { SECURITY_ENTITY_RISK_SCORE_TOOL_ID, SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, SECURITY_LABS_SEARCH_TOOL_ID, - SECURITY_ALERTS_TOOL_ID, platformCoreTools.cases, platformCoreTools.generateEsql, ], diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity.ts similarity index 93% rename from x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts rename to x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity.ts index 7cabb6af9430f..664ba08a12908 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity_risk.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity.ts @@ -30,11 +30,11 @@ const isEntityRiskFormattedData = (data: unknown): data is string => { }; /** - * Creates the definition for the `risk_entity` attachment type. + * Creates the definition for the `entity` attachment type. */ -export const createEntityRiskAttachmentType = (): AttachmentTypeDefinition => { +export const createEntityAttachmentType = (): AttachmentTypeDefinition => { return { - id: SecurityAgentBuilderAttachments.risk_entity, + id: SecurityAgentBuilderAttachments.entity, validate: (input) => { const parseResult = riskEntityAttachmentDataSchema.safeParse(input); if (parseResult.success) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts index 6644958f09171..a48b74eeef58d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts @@ -6,5 +6,5 @@ */ export { createAlertAttachmentType } from './alert'; -export { createEntityRiskAttachmentType } from './entity_risk'; +export { createEntityAttachmentType } from './entity'; export { createQueryHelpAttachmentType } from './query_help'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts index fc09e9b2134f9..4bac1a57eb143 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts @@ -7,7 +7,7 @@ import type { OnechatPluginSetup } from '@kbn/onechat-plugin/server'; import { createAlertAttachmentType } from './alert'; -import { createEntityRiskAttachmentType } from './entity_risk'; +import { createEntityAttachmentType } from './entity'; import { createQueryHelpAttachmentType } from './query_help'; /** @@ -15,6 +15,6 @@ import { createQueryHelpAttachmentType } from './query_help'; */ export const registerAttachments = async (onechat: OnechatPluginSetup) => { onechat.attachments.registerType(createAlertAttachmentType()); - onechat.attachments.registerType(createEntityRiskAttachmentType()); + onechat.attachments.registerType(createEntityAttachmentType()); onechat.attachments.registerType(createQueryHelpAttachmentType()); }; From 6b26745c58281807e3d6f283ba666422c7c6c88b Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 12:04:32 +0100 Subject: [PATCH 60/96] testing --- .../new_agent_builder_attachment.test.tsx | 112 ++++++++ .../public/agent_builder/helpers.test.tsx | 63 +++++ .../use_agent_builder_attachment.test.tsx | 159 ++++++++++++ .../public/agent_builder/jest.config.js | 20 ++ .../agent_builder/__mocks__/test_helpers.ts | 122 +++++++++ .../agent_builder/attachments/alert.test.ts | 111 ++++++++ .../agent_builder/attachments/entity.test.ts | 156 ++++++++++++ .../attachments/query_help.test.ts | 114 +++++++++ .../server/agent_builder/jest.config.js | 20 ++ .../attack_discovery_search_tool.test.ts | 162 ++++++++++++ .../tools/entity_risk_score_tool.test.ts | 240 ++++++++++++++++++ .../agent_builder/tools/helpers.test.ts | 43 ++++ .../tools/security_labs_search_tool.test.ts | 215 ++++++++++++++++ 13 files changed, 1537 insertions(+) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/agent_builder/jest.config.js create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/__mocks__/test_helpers.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.test.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.test.tsx new file mode 100644 index 0000000000000..291b561b5b452 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.test.tsx @@ -0,0 +1,112 @@ +/* + * 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. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { TestProviders } from '../../common/mock'; +import { NewAgentBuilderAttachment } from './new_agent_builder_attachment'; +import * as i18n from './translations'; + +describe('NewAgentBuilderAttachment', () => { + const defaultProps = { + onClick: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders with default props', () => { + render( + + + + ); + + expect(screen.getByTestId('newAgentBuilderAttachment')).toBeInTheDocument(); + expect(screen.getByText(i18n.VIEW_IN_AGENT_BUILDER)).toBeInTheDocument(); + }); + + it('renders with custom color', () => { + render( + + + + ); + + const button = screen.getByTestId('newAgentBuilderAttachment'); + expect(button).toBeInTheDocument(); + }); + + it('renders with custom iconType', () => { + render( + + + + ); + + expect(screen.getByTestId('newAgentBuilderAttachment')).toBeInTheDocument(); + }); + + it('renders with custom size', () => { + render( + + + + ); + + const button = screen.getByTestId('newAgentBuilderAttachment'); + expect(button).toBeInTheDocument(); + }); + + it('renders with custom text', () => { + const customText = 'Custom Button Text'; + render( + + + + ); + + expect(screen.getByText(customText)).toBeInTheDocument(); + }); + + it('calls onClick callback when button is clicked', () => { + const onClick = jest.fn(); + render( + + + + ); + + screen.getByTestId('newAgentBuilderAttachment').click(); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('has correct aria-label attribute', () => { + const customText = 'Custom Label'; + render( + + + + ); + + const button = screen.getByTestId('newAgentBuilderAttachment'); + expect(button).toHaveAttribute('aria-label', customText); + }); + + it('has correct data-test-subj attribute', () => { + render( + + + + ); + + expect(screen.getByTestId('newAgentBuilderAttachment')).toBeInTheDocument(); + }); +}); + diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx new file mode 100644 index 0000000000000..2b04cc8f18c0c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx @@ -0,0 +1,63 @@ +/* + * 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. + */ + +import { ESSENTIAL_ALERT_FIELDS } from '../../common'; +import { filterAndStringifyAlertData } from './helpers'; + +describe('filterAndStringifyAlertData', () => { + it('filters to essential fields only', () => { + const rawData: Record = { + [ESSENTIAL_ALERT_FIELDS[0]]: ['value1'], + [ESSENTIAL_ALERT_FIELDS[1]]: ['value2'], + 'nonEssentialField': ['shouldBeExcluded'], + 'anotherNonEssential': ['shouldAlsoBeExcluded'], + }; + + const result = filterAndStringifyAlertData(rawData); + const parsed = JSON.parse(result); + + expect(parsed).toHaveProperty(ESSENTIAL_ALERT_FIELDS[0]); + expect(parsed).toHaveProperty(ESSENTIAL_ALERT_FIELDS[1]); + expect(parsed).not.toHaveProperty('nonEssentialField'); + expect(parsed).not.toHaveProperty('anotherNonEssential'); + }); + + it('excludes non-essential fields', () => { + const rawData: Record = { + 'field1': ['value1'], + 'field2': ['value2'], + }; + + const result = filterAndStringifyAlertData(rawData); + const parsed = JSON.parse(result); + + expect(Object.keys(parsed).length).toBe(0); + }); + + it('returns valid JSON string', () => { + const rawData: Record = { + [ESSENTIAL_ALERT_FIELDS[0]]: ['value1'], + }; + + const result = filterAndStringifyAlertData(rawData); + + expect(() => JSON.parse(result)).not.toThrow(); + expect(JSON.parse(result)).toEqual({ + [ESSENTIAL_ALERT_FIELDS[0]]: ['value1'], + }); + }); + + it('handles empty input', () => { + const rawData: Record = {}; + + const result = filterAndStringifyAlertData(rawData); + const parsed = JSON.parse(result); + + expect(parsed).toEqual({}); + }); +}); + diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx new file mode 100644 index 0000000000000..b76e5d7d30cdf --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx @@ -0,0 +1,159 @@ +/* + * 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. + */ + +import { renderHook, act } from '@testing-library/react'; +import React from 'react'; +import { TestProviders } from '../../common/mock'; +import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; +import { useAgentBuilderAttachment } from './use_agent_builder_attachment'; + +const mockOpenConversationFlyout = jest.fn(); + +const createWrapper = (onechatService: typeof mockOnechatService | null = mockOnechatService) => { + const mockStartServices = createStartServicesMock(); + const startServices = { + ...mockStartServices, + onechat: onechatService ?? undefined, + }; + + return ({ children }: { children: React.ReactNode }) => ( + {children} + ); +}; + +const mockOnechatService = { + openConversationFlyout: mockOpenConversationFlyout, + tools: {} as any, + setConversationFlyoutActiveConfig: jest.fn(), + clearConversationFlyoutActiveConfig: jest.fn(), +}; + +describe('useAgentBuilderAttachment', () => { + const defaultParams = { + attachmentType: 'alert', + attachmentData: { alert: 'test alert data' }, + attachmentPrompt: 'Analyze this alert', + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(Date, 'now').mockReturnValue(1234567890); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('returns openAgentBuilderFlyout function', () => { + const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { + wrapper: createWrapper(), + }); + + expect(result.current.openAgentBuilderFlyout).toBeDefined(); + expect(typeof result.current.openAgentBuilderFlyout).toBe('function'); + }); + + it('opens flyout with correct attachment data and prompt', () => { + const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.openAgentBuilderFlyout(); + }); + + expect(mockOpenConversationFlyout).toHaveBeenCalledTimes(1); + expect(mockOpenConversationFlyout).toHaveBeenCalledWith({ + newConversation: true, + initialMessage: 'Analyze this alert', + attachments: [ + { + id: 'alert-1234567890', + type: 'alert', + getContent: expect.any(Function), + }, + ], + sessionTag: 'security', + }); + }); + + it('opens flyout with correct sessionTag', () => { + const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.openAgentBuilderFlyout(); + }); + + expect(mockOpenConversationFlyout).toHaveBeenCalledWith( + expect.objectContaining({ + sessionTag: 'security', + }) + ); + }); + + it('handles missing onechat service gracefully', () => { + const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { + wrapper: createWrapper(null), + }); + + act(() => { + result.current.openAgentBuilderFlyout(); + }); + + expect(mockOpenConversationFlyout).not.toHaveBeenCalled(); + }); + + it('handles missing openConversationFlyout method gracefully', () => { + const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { + wrapper: createWrapper({ + ...mockOnechatService, + openConversationFlyout: undefined, + } as Partial as typeof mockOnechatService), + }); + + act(() => { + result.current.openAgentBuilderFlyout(); + }); + + expect(mockOpenConversationFlyout).not.toHaveBeenCalled(); + }); + + it('generates attachment ID with timestamp', async () => { + const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.openAgentBuilderFlyout(); + }); + + const callArgs = mockOpenConversationFlyout.mock.calls[0][0]; + const attachment = callArgs.attachments[0]; + + expect(attachment.id).toBe('alert-1234567890'); + expect(attachment.id).toMatch(/^alert-\d+$/); + }); + + it('attachment getContent returns correct data', async () => { + const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.openAgentBuilderFlyout(); + }); + + const callArgs = mockOpenConversationFlyout.mock.calls[0][0]; + const attachment = callArgs.attachments[0]; + const content = await attachment.getContent(); + + expect(content).toEqual({ alert: 'test alert data' }); + }); +}); + diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/jest.config.js b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/jest.config.js new file mode 100644 index 0000000000000..6d301f21ad66f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/jest.config.js @@ -0,0 +1,20 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../../..', + roots: ['/x-pack/solutions/security/plugins/security_solution/public/agent_builder'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/solutions/security/plugins/security_solution/public/agent_builder', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/solutions/security/plugins/security_solution/public/agent_builder/**/*.{ts,tsx}', + ], + moduleNameMapper: require('../../server/__mocks__/module_name_map'), +}; + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/__mocks__/test_helpers.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/__mocks__/test_helpers.ts new file mode 100644 index 0000000000000..304bc8fa94243 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/__mocks__/test_helpers.ts @@ -0,0 +1,122 @@ +/* + * 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. + */ + +import { httpServerMock } from '@kbn/core-http-server-mocks'; +import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; +import { coreMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { uiSettingsServiceMock } from '@kbn/core-ui-settings-server-mocks'; +import type { ToolHandlerContext, ToolAvailabilityContext } from '@kbn/onechat-server/tools'; +import type { + ModelProvider, + ToolProvider, + ScopedRunner, + ToolResultStore, + ToolEventEmitter, +} from '@kbn/onechat-server'; + +/** + * Creates common mocks for tool tests + */ +export const createToolTestMocks = () => { + const mockCore = coreMock.createSetup(); + const mockLogger = loggingSystemMock.createLogger(); + const mockEsClient = elasticsearchClientMock.createScopedClusterClient(); + const mockRequest = httpServerMock.createKibanaRequest({ + path: '/s/default/app/security', + }); + + return { + mockCore, + mockLogger, + mockEsClient, + mockRequest, + }; +}; + +/** + * Sets up mock core start services for tools that need it + */ +export const setupMockCoreStartServices = ( + mockCore: ReturnType, + mockEsClient: ReturnType +) => { + const mockCoreStart = coreMock.createStart(); + Object.assign(mockCoreStart.elasticsearch.client, { + asInternalUser: mockEsClient.asInternalUser, + asCurrentUser: mockEsClient.asCurrentUser, + }); + mockCore.getStartServices.mockResolvedValue([mockCoreStart, {}, {}]); +}; + +/** + * Creates minimal mocks for ToolHandlerContext fields + */ +const createMockModelProvider = (): ModelProvider => + ({ + getDefaultModel: jest.fn(), + getModel: jest.fn(), + getUsageStats: jest.fn().mockReturnValue({ calls: [] }), + } as unknown as ModelProvider); + +const createMockToolProvider = (): ToolProvider => + ({ + has: jest.fn(), + get: jest.fn(), + list: jest.fn(), + } as unknown as ToolProvider); + +const createMockScopedRunner = (): ScopedRunner => + ({ + runTools: jest.fn(), + } as unknown as ScopedRunner); + +const createMockToolResultStore = (): ToolResultStore => + ({ + get: jest.fn(), + } as unknown as ToolResultStore); + +const createMockToolEventEmitter = (): ToolEventEmitter => + ({ + reportProgress: jest.fn(), + } as unknown as ToolEventEmitter); + +/** + * Creates a tool handler context object + */ +export const createToolHandlerContext = ( + mockRequest: ReturnType, + mockEsClient: ReturnType, + mockLogger: ReturnType, + additionalContext: Partial> = {} +): ToolHandlerContext => { + return { + request: mockRequest, + esClient: mockEsClient, + logger: mockLogger, + spaceId: 'default', + modelProvider: additionalContext.modelProvider ?? createMockModelProvider(), + toolProvider: additionalContext.toolProvider ?? createMockToolProvider(), + runner: additionalContext.runner ?? createMockScopedRunner(), + resultStore: additionalContext.resultStore ?? createMockToolResultStore(), + events: additionalContext.events ?? createMockToolEventEmitter(), + }; +}; + +/** + * Creates a tool availability context object + */ +export const createToolAvailabilityContext = ( + mockRequest: ReturnType, + spaceId: string, + uiSettings?: ToolAvailabilityContext['uiSettings'] +): ToolAvailabilityContext => { + return { + request: mockRequest, + spaceId, + uiSettings: uiSettings ?? uiSettingsServiceMock.createClient(), + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.test.ts new file mode 100644 index 0000000000000..cf005b6ce9c49 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.test.ts @@ -0,0 +1,111 @@ +/* + * 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. + */ + +import type { Attachment } from '@kbn/onechat-common/attachments'; +import { platformCoreTools } from '@kbn/onechat-common'; +import { SecurityAgentBuilderAttachments } from '../../../common/constants'; +import { + SECURITY_ENTITY_RISK_SCORE_TOOL_ID, + SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, + SECURITY_LABS_SEARCH_TOOL_ID, +} from '../tools'; +import { createAlertAttachmentType } from './alert'; + +describe('createAlertAttachmentType', () => { + const attachmentType = createAlertAttachmentType(); + + describe('validate', () => { + it('returns valid when alert data is valid', async () => { + const input = { alert: 'test alert data' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.data).toEqual(input); + } + }); + + it('returns invalid when alert field is missing', async () => { + const input = {}; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toBeDefined(); + } + }); + + it('returns invalid when alert field is not a string', async () => { + const input = { alert: 123 }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toBeDefined(); + } + }); + }); + + describe('format', () => { + it('returns correct text representation', async () => { + const attachment: Attachment = { + id: 'test-id', + type: SecurityAgentBuilderAttachments.alert, + data: { alert: 'test alert content' }, + }; + + const formatted = await attachmentType.format(attachment); + const representation = await formatted.getRepresentation(); + + expect(representation.type).toBe('text'); + expect(representation.value).toBe('test alert content'); + }); + + it('throws error when attachment data is invalid', () => { + const attachment: Attachment = { + id: 'test-id', + type: SecurityAgentBuilderAttachments.alert, + data: { invalid: 'data' }, + }; + + expect(() => attachmentType.format(attachment)).toThrow( + 'Invalid alert attachment data for attachment test-id' + ); + }); + }); + + describe('getTools', () => { + it('returns expected tool IDs', () => { + const tools = attachmentType.getTools?.(); + + expect(tools).toBeDefined(); + if (tools) { + expect(tools).toContain(SECURITY_ENTITY_RISK_SCORE_TOOL_ID); + expect(tools).toContain(SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID); + expect(tools).toContain(SECURITY_LABS_SEARCH_TOOL_ID); + expect(tools).toContain(platformCoreTools.cases); + expect(tools).toContain(platformCoreTools.generateEsql); + } + }); + }); + + describe('getAgentDescription', () => { + it('returns expected description', () => { + const description = attachmentType.getAgentDescription?.(); + + expect(description).toContain('security alert data'); + expect(description).toContain('SECURITY ALERT DATA'); + expect(description).toContain('Extract alert id(s): _id'); + expect(description).toContain('Extract rule name: kibana.alert.rule.name'); + expect(description).toContain('Extract entities: host.name, user.name, service.name'); + }); + }); +}); + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity.test.ts new file mode 100644 index 0000000000000..f579367ce3c8b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity.test.ts @@ -0,0 +1,156 @@ +/* + * 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. + */ + +import type { Attachment } from '@kbn/onechat-common/attachments'; +import { SecurityAgentBuilderAttachments } from '../../../common/constants'; +import { SECURITY_ENTITY_RISK_SCORE_TOOL_ID } from '../tools'; +import { createEntityAttachmentType } from './entity'; + +describe('createEntityAttachmentType', () => { + const attachmentType = createEntityAttachmentType(); + + describe('validate', () => { + it('returns valid when entity data is valid with host identifierType', async () => { + const input = { identifierType: 'host', identifier: 'hostname-1' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.data).toBe('identifier: hostname-1, identifierType: host'); + } + }); + + it('returns valid when entity data is valid with user identifierType', async () => { + const input = { identifierType: 'user', identifier: 'username-1' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.data).toBe('identifier: username-1, identifierType: user'); + } + }); + + it('returns valid when entity data is valid with service identifierType', async () => { + const input = { identifierType: 'service', identifier: 'service-1' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.data).toBe('identifier: service-1, identifierType: service'); + } + }); + + it('returns valid when entity data is valid with generic identifierType', async () => { + const input = { identifierType: 'generic', identifier: 'generic-1' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.data).toBe('identifier: generic-1, identifierType: generic'); + } + }); + + it('returns invalid when identifierType is missing', async () => { + const input = { identifier: 'test-identifier' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toBeDefined(); + } + }); + + it('returns invalid when identifier is missing', async () => { + const input = { identifierType: 'host' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toBeDefined(); + } + }); + + it('returns invalid when identifier is empty string', async () => { + const input = { identifierType: 'host', identifier: '' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toBeDefined(); + } + }); + + it('returns invalid when identifierType is not in enum', async () => { + const input = { identifierType: 'invalid', identifier: 'test' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toBeDefined(); + } + }); + }); + + describe('format', () => { + it('returns correct string format', async () => { + const attachment: Attachment = { + id: 'test-id', + type: SecurityAgentBuilderAttachments.entity, + data: 'identifier: hostname-1, identifierType: host', + }; + + const formatted = await attachmentType.format(attachment); + const representation = await formatted.getRepresentation(); + + expect(representation.type).toBe('text'); + expect(representation.value).toBe('identifier: hostname-1, identifierType: host'); + }); + + it('throws error when attachment data is invalid', () => { + const attachment: Attachment = { + id: 'test-id', + type: SecurityAgentBuilderAttachments.entity, + data: 'invalid data', + }; + + expect(() => attachmentType.format(attachment)).toThrow( + 'Invalid risk entity attachment data for attachment test-id' + ); + }); + }); + + describe('getTools', () => { + it('returns entity risk score tool', () => { + const tools = attachmentType.getTools?.(); + + expect(tools).toBeDefined(); + if (tools) { + expect(tools).toEqual([SECURITY_ENTITY_RISK_SCORE_TOOL_ID]); + } + }); + }); + + describe('getAgentDescription', () => { + it('returns expected description', () => { + const description = attachmentType.getAgentDescription?.(); + + expect(description).toContain('risk entity'); + expect(description).toContain('RISK ENTITY DATA'); + expect(description).toContain('identifierType'); + expect(description).toContain('identifier'); + }); + }); +}); + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts new file mode 100644 index 0000000000000..ae04f02b8f781 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts @@ -0,0 +1,114 @@ +/* + * 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. + */ + +import type { Attachment } from '@kbn/onechat-common/attachments'; +import { platformCoreTools } from '@kbn/onechat-common'; +import { SecurityAgentBuilderAttachments } from '../../../common/constants'; +import { createQueryHelpAttachmentType } from './query_help'; + +describe('createQueryHelpAttachmentType', () => { + const attachmentType = createQueryHelpAttachmentType(); + + describe('validate', () => { + it('returns valid when query help data is valid', async () => { + const input = { query: 'SELECT * FROM test', queryLanguage: 'esql' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.data).toEqual(input); + } + }); + + it('returns invalid when query field is missing', async () => { + const input = { queryLanguage: 'esql' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toBeDefined(); + } + }); + + it('returns invalid when queryLanguage field is missing', async () => { + const input = { query: 'SELECT * FROM test' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toBeDefined(); + } + }); + + it('returns invalid when query is not a string', async () => { + const input = { query: 123, queryLanguage: 'esql' }; + + const result = await attachmentType.validate(input); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.error).toBeDefined(); + } + }); + }); + + describe('format', () => { + it('includes query and queryLanguage in formatted output', async () => { + const attachment: Attachment = { + id: 'test-id', + type: SecurityAgentBuilderAttachments.query_help, + data: { query: 'SELECT * FROM test', queryLanguage: 'esql' }, + }; + + const formatted = await attachmentType.format(attachment); + const representation = await formatted.getRepresentation(); + + expect(representation.type).toBe('text'); + expect(representation.value).toContain('Query: SELECT * FROM test'); + expect(representation.value).toContain('Query Language: esql'); + }); + + it('throws error when attachment data is invalid', () => { + const attachment: Attachment = { + id: 'test-id', + type: SecurityAgentBuilderAttachments.query_help, + data: { invalid: 'data' }, + }; + + expect(() => attachmentType.format(attachment)).toThrow( + 'Invalid query help attachment data for attachment test-id' + ); + }); + }); + + describe('getTools', () => { + it('returns expected tools', () => { + const tools = attachmentType.getTools?.(); + + expect(tools).toBeDefined(); + if (tools) { + expect(tools).toContain(platformCoreTools.generateEsql); + expect(tools).toContain('platformCoreTools.productDocumentation'); + } + }); + }); + + describe('getAgentDescription', () => { + it('returns expected description', () => { + const description = attachmentType.getAgentDescription?.(); + + expect(description).toContain('broken query'); + expect(description).toContain('QUERY HELP DATA'); + expect(description).toContain('queryLanguage'); + expect(description).toContain('generateEsql'); + }); + }); +}); + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js new file mode 100644 index 0000000000000..a6a38d771b361 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js @@ -0,0 +1,20 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../../..', + roots: ['/x-pack/solutions/security/plugins/security_solution/server/agent_builder'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/solutions/security/plugins/security_solution/server/agent_builder', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/solutions/security/plugins/security_solution/server/agent_builder/**/*.{ts,tsx}', + ], + moduleNameMapper: require('../__mocks__/module_name_map'), +}; + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts new file mode 100644 index 0000000000000..b6f96741e4561 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts @@ -0,0 +1,162 @@ +/* + * 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. + */ + +import { ToolResultType, type TabularDataResult, type ErrorResult } from '@kbn/onechat-common'; +import { executeEsql } from '@kbn/onechat-genai-utils'; +import { createToolHandlerContext, createToolTestMocks } from '../__mocks__/test_helpers'; +import { attackDiscoverySearchTool } from './attack_discovery_search_tool'; + +jest.mock('@kbn/onechat-genai-utils', () => ({ + executeEsql: jest.fn(), +})); + +describe('attackDiscoverySearchTool', () => { + const { mockLogger, mockEsClient, mockRequest } = createToolTestMocks(); + const tool = attackDiscoverySearchTool(); + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-01-15T12:00:00Z')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('schema', () => { + it('validates correct schema with alertIds array', () => { + const validInput = { + alertIds: ['alert-1', 'alert-2'], + }; + + const result = tool.schema.safeParse(validInput); + + expect(result.success).toBe(true); + }); + + it('rejects non-array alertIds', () => { + const invalidInput = { + alertIds: 'not-an-array', + }; + + const result = tool.schema.safeParse(invalidInput); + + expect(result.success).toBe(false); + }); + + it('rejects empty alertIds array', () => { + const invalidInput = { + alertIds: [], + }; + + const result = tool.schema.safeParse(invalidInput); + + expect(result.success).toBe(true); + }); + }); + + describe('handler', () => { + it('builds correct ES|QL query with date filter and alert IDs', async () => { + const mockEsqlResponse = { + columns: [{ name: '_id', type: 'keyword' }], + values: [['attack-discovery-1']], + }; + (executeEsql as jest.Mock).mockResolvedValue(mockEsqlResponse); + + await tool.handler( + { alertIds: ['alert-1', 'alert-2'] }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + expect(executeEsql).toHaveBeenCalled(); + const callArgs = (executeEsql as jest.Mock).mock.calls[0][0]; + expect(callArgs.query).toContain('FROM .alerts-security.attack.discovery.alerts-default*'); + expect(callArgs.query).toContain('MV_CONTAINS(kibana.alert.attack_discovery.alert_ids,"alert-1")'); + expect(callArgs.query).toContain('MV_CONTAINS(kibana.alert.attack_discovery.alert_ids,"alert-2")'); + expect(callArgs.query).toContain('@timestamp >='); + expect(callArgs.query).toContain('LIMIT 100'); + }); + + it('executes ES|QL query and returns tabular data', async () => { + const mockEsqlResponse = { + columns: [ + { name: '_id', type: 'keyword' }, + { name: 'kibana.alert.attack_discovery.title', type: 'keyword' }, + ], + values: [ + ['attack-discovery-1', 'Test Attack Discovery'], + ['attack-discovery-2', 'Another Attack Discovery'], + ], + }; + (executeEsql as jest.Mock).mockResolvedValue(mockEsqlResponse); + + const result = await tool.handler( + { alertIds: ['alert-1'] }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + expect(result.results).toHaveLength(2); + expect(result.results[0].type).toBe(ToolResultType.query); + const tabularResult = result.results[1] as TabularDataResult; + expect(tabularResult.type).toBe(ToolResultType.tabularData); + expect(tabularResult.data.columns).toEqual(mockEsqlResponse.columns); + expect(tabularResult.data.values).toEqual(mockEsqlResponse.values); + }); + + it('limits results appropriately', async () => { + const mockEsqlResponse = { + columns: [{ name: '_id', type: 'keyword' }], + values: Array.from({ length: 100 }, (_, i) => [`attack-discovery-${i}`]), + }; + (executeEsql as jest.Mock).mockResolvedValue(mockEsqlResponse); + + await tool.handler( + { alertIds: ['alert-1'] }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + const callArgs = (executeEsql as jest.Mock).mock.calls[0][0]; + expect(callArgs.query).toContain('LIMIT 100'); + }); + + it('handles query failures', async () => { + const error = new Error('ES|QL query failed'); + (executeEsql as jest.Mock).mockRejectedValue(error); + + const result = await tool.handler( + { alertIds: ['alert-1'] }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + expect(result.results).toHaveLength(1); + const errorResult = result.results[0] as ErrorResult; + expect(errorResult.type).toBe(ToolResultType.error); + expect(errorResult.data.message).toContain('Error: ES|QL query failed'); + expect(mockLogger.error).toHaveBeenCalled(); + }); + + it('builds date filter for last 7 days', async () => { + const mockEsqlResponse = { + columns: [{ name: '_id', type: 'keyword' }], + values: [], + }; + (executeEsql as jest.Mock).mockResolvedValue(mockEsqlResponse); + + await tool.handler( + { alertIds: ['alert-1'] }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + const callArgs = (executeEsql as jest.Mock).mock.calls[0][0]; + const query = callArgs.query; + expect(query).toContain('@timestamp >='); + expect(query).toContain('@timestamp <='); + }); + }); +}); + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts new file mode 100644 index 0000000000000..46fdb0bd295fa --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts @@ -0,0 +1,240 @@ +/* + * 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. + */ + +import { ToolResultType, type ErrorResult, type OtherResult } from '@kbn/onechat-common'; +import { DEFAULT_ALERTS_INDEX } from '../../../common/constants'; +import { getRiskIndex } from '../../../common/search_strategy/security_solution/risk_score/common'; +import { + createToolAvailabilityContext, + createToolHandlerContext, + createToolTestMocks, + setupMockCoreStartServices, +} from '../__mocks__/test_helpers'; +import { entityRiskScoreTool } from './entity_risk_score_tool'; + +jest.mock('../../lib/entity_analytics/risk_score/get_risk_score', () => ({ + createGetRiskScores: jest.fn(() => jest.fn()), +})); + +describe('entityRiskScoreTool', () => { + const { mockCore, mockLogger, mockEsClient, mockRequest } = createToolTestMocks(); + const tool = entityRiskScoreTool(mockCore); + + beforeEach(() => { + jest.clearAllMocks(); + setupMockCoreStartServices(mockCore, mockEsClient); + }); + + describe('schema', () => { + it('validates correct schema', () => { + const validInput = { + identifierType: 'host', + identifier: 'hostname-1', + }; + + const result = tool.schema.safeParse(validInput); + + expect(result.success).toBe(true); + }); + + it('rejects invalid identifierType', () => { + const invalidInput = { + identifierType: 'invalid', + identifier: 'hostname-1', + }; + + const result = tool.schema.safeParse(invalidInput); + + expect(result.success).toBe(false); + }); + + it('rejects empty identifier', () => { + const invalidInput = { + identifierType: 'host', + identifier: '', + }; + + const result = tool.schema.safeParse(invalidInput); + + expect(result.success).toBe(false); + }); + }); + + describe('availability', () => { + it('returns available when risk index exists', async () => { + mockEsClient.asInternalUser.indices.exists.mockResolvedValue(true); + + const result = await tool.availability!.handler( + createToolAvailabilityContext(mockRequest, 'default') + ); + + expect(result.status).toBe('available'); + expect(mockEsClient.asInternalUser.indices.exists).toHaveBeenCalledWith({ + index: getRiskIndex('default', true), + }); + }); + + it('returns unavailable when risk index does not exist', async () => { + mockEsClient.asInternalUser.indices.exists.mockResolvedValue(false); + + const result = await tool.availability!.handler( + createToolAvailabilityContext(mockRequest, 'default') + ); + + expect(result.status).toBe('unavailable'); + expect(result.reason).toBe('Risk score index does not exist for this space'); + }); + + it('returns unavailable when index check throws error', async () => { + mockEsClient.asInternalUser.indices.exists.mockRejectedValue(new Error('ES error')); + + const result = await tool.availability!.handler( + createToolAvailabilityContext(mockRequest, 'default') + ); + + expect(result.status).toBe('unavailable'); + expect(result.reason).toContain('Failed to check risk score index availability'); + }); + }); + + describe('handler', () => { + const { createGetRiskScores } = require('../../lib/entity_analytics/risk_score/get_risk_score'); + + beforeEach(() => { + createGetRiskScores.mockReturnValue(jest.fn()); + }); + + it('successfully fetches risk score with valid identifierType and identifier', async () => { + const mockGetRiskScores = jest.fn().mockResolvedValue([ + { + id: 'risk-1', + calculated_score_norm: 75, + inputs: [ + { + id: 'alert-1', + risk_score: 50, + contribution_score: 25, + category: 'alerts', + }, + ], + }, + ]); + createGetRiskScores.mockReturnValue(mockGetRiskScores); + + mockEsClient.asCurrentUser.search.mockResolvedValue({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + hits: [ + { + _id: 'alert-1', + _index: 'test-index', + _source: { 'kibana.alert.rule.name': 'Test Rule' }, + }, + ], + total: { value: 1, relation: 'eq' }, + }, + }); + + const result = await tool.handler( + { identifierType: 'host', identifier: 'hostname-1' }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + expect(result.results).toHaveLength(1); + expect(result.results[0].type).toBe(ToolResultType.other); + if (result.results[0].type === ToolResultType.other) { + expect(result.results[0].data).toHaveProperty('riskScore'); + } + expect(mockGetRiskScores).toHaveBeenCalledWith({ + entityType: 'host', + entityIdentifier: 'hostname-1', + pagination: { querySize: 1, cursorStart: 0 }, + }); + }); + + it('returns error when no risk score found', async () => { + const mockGetRiskScores = jest.fn().mockResolvedValue([]); + createGetRiskScores.mockReturnValue(mockGetRiskScores); + + const result = await tool.handler( + { identifierType: 'user', identifier: 'username-1' }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + expect(result.results).toHaveLength(1); + const errorResult = result.results[0] as ErrorResult; + expect(errorResult.type).toBe(ToolResultType.error); + expect(errorResult.data.message).toContain('No risk score found'); + }); + + it('enhances inputs with alert data', async () => { + const mockGetRiskScores = jest.fn().mockResolvedValue([ + { + id: 'risk-1', + calculated_score_norm: 75, + inputs: [ + { + id: 'alert-1', + risk_score: 50, + contribution_score: 25, + category: 'alerts', + }, + ], + }, + ]); + createGetRiskScores.mockReturnValue(mockGetRiskScores); + + const alertData = { 'kibana.alert.rule.name': 'Test Rule' }; + mockEsClient.asCurrentUser.search.mockResolvedValue({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + hits: [{ _id: 'alert-1', _index: 'test-index', _source: alertData }], + total: { value: 1, relation: 'eq' }, + }, + }); + + const result = await tool.handler( + { identifierType: 'host', identifier: 'hostname-1' }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + const otherResult = result.results[0] as OtherResult; + expect(otherResult.type).toBe(ToolResultType.other); + const riskScore = otherResult.data.riskScore as { + inputs: Array<{ alert_contribution?: unknown }>; + }; + expect(riskScore.inputs[0].alert_contribution).toEqual(alertData); + expect(mockEsClient.asCurrentUser.search).toHaveBeenCalledWith( + expect.objectContaining({ + index: `${DEFAULT_ALERTS_INDEX}-default`, + _source: expect.any(Array), + }) + ); + }); + + it('handles ES client failures', async () => { + const mockGetRiskScores = jest.fn().mockRejectedValue(new Error('ES error')); + createGetRiskScores.mockReturnValue(mockGetRiskScores); + + const result = await tool.handler( + { identifierType: 'host', identifier: 'hostname-1' }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + expect(result.results).toHaveLength(1); + const errorResult = result.results[0] as ErrorResult; + expect(errorResult.type).toBe(ToolResultType.error); + expect(errorResult.data.message).toContain('Error fetching risk score'); + expect(mockLogger.error).toHaveBeenCalled(); + }); + }); +}); + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts new file mode 100644 index 0000000000000..d18529d9d3737 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts @@ -0,0 +1,43 @@ +/* + * 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. + */ + +import { httpServerMock } from '@kbn/core-http-server-mocks'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; +import { getSpaceIdFromRequest } from './helpers'; + +describe('getSpaceIdFromRequest', () => { + it('returns space ID from request path', () => { + const request = httpServerMock.createKibanaRequest({ + path: '/s/custom-space/app/security', + }); + + const spaceId = getSpaceIdFromRequest(request); + + expect(spaceId).toBe('custom-space'); + }); + + it('returns default space ID when no space ID in path', () => { + const request = httpServerMock.createKibanaRequest({ + path: '/app/security', + }); + + const spaceId = getSpaceIdFromRequest(request); + + expect(spaceId).toBe(DEFAULT_SPACE_ID); + }); + + it('returns default space ID when path is root', () => { + const request = httpServerMock.createKibanaRequest({ + path: '/', + }); + + const spaceId = getSpaceIdFromRequest(request); + + expect(spaceId).toBe(DEFAULT_SPACE_ID); + }); +}); + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.test.ts new file mode 100644 index 0000000000000..2c8e47f123d5b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.test.ts @@ -0,0 +1,215 @@ +/* + * 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. + */ + +import { ToolResultType, type ErrorResult } from '@kbn/onechat-common'; +import type { ToolHandlerContext } from '@kbn/onechat-server/tools'; +import { SECURITY_LABS_RESOURCE } from '@kbn/elastic-assistant-plugin/server/routes/knowledge_base/constants'; +import { runSearchTool } from '@kbn/onechat-genai-utils/tools/search/run_search_tool'; +import { + createToolAvailabilityContext, + createToolHandlerContext, + createToolTestMocks, + setupMockCoreStartServices, +} from '../__mocks__/test_helpers'; +import { securityLabsSearchTool } from './security_labs_search_tool'; + +jest.mock('@kbn/onechat-genai-utils/tools/search/run_search_tool', () => ({ + runSearchTool: jest.fn(), +})); + +describe('securityLabsSearchTool', () => { + const { mockCore, mockLogger, mockEsClient, mockRequest } = createToolTestMocks(); + const mockModelProvider = { + getDefaultModel: jest.fn().mockResolvedValue({ model: 'test-model' }), + getModel: jest.fn(), + getUsageStats: jest.fn().mockReturnValue({ calls: [] }), + }; + const mockEvents = { + reportProgress: jest.fn(), + }; + const tool = securityLabsSearchTool(mockCore); + + beforeEach(() => { + jest.clearAllMocks(); + setupMockCoreStartServices(mockCore, mockEsClient); + }); + + describe('schema', () => { + it('validates correct schema', () => { + const validInput = { + query: 'test query', + }; + + const result = tool.schema.safeParse(validInput); + + expect(result.success).toBe(true); + }); + + it('rejects missing query', () => { + const invalidInput = {}; + + const result = tool.schema.safeParse(invalidInput); + + expect(result.success).toBe(false); + }); + + it('rejects non-string query', () => { + const invalidInput = { + query: 123, + }; + + const result = tool.schema.safeParse(invalidInput); + + expect(result.success).toBe(false); + }); + }); + + describe('availability', () => { + it('returns available when Security Labs content exists', async () => { + mockEsClient.asInternalUser.search.mockResolvedValue({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + hits: [{ _id: 'test-id', _index: 'test-index', _source: { kb_resource: SECURITY_LABS_RESOURCE } }], + total: { value: 1, relation: 'eq' }, + }, + }); + + const result = await tool.availability!.handler( + createToolAvailabilityContext(mockRequest, 'default') + ); + + expect(result.status).toBe('available'); + expect(mockEsClient.asInternalUser.search).toHaveBeenCalledWith( + expect.objectContaining({ + index: '.kibana-elastic-ai-assistant-knowledge-base-default', + query: expect.objectContaining({ + bool: { + filter: [ + { + term: { + kb_resource: SECURITY_LABS_RESOURCE, + }, + }, + ], + }, + }), + }) + ); + }); + + it('returns unavailable when Security Labs content not found', async () => { + mockEsClient.asInternalUser.search.mockResolvedValue({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + hits: [], + total: { value: 0, relation: 'eq' }, + }, + }); + + const result = await tool.availability!.handler( + createToolAvailabilityContext(mockRequest, 'default') + ); + + expect(result.status).toBe('unavailable'); + expect(result.reason).toBe('Security Labs content not found in knowledge base'); + }); + + it('returns unavailable when availability check throws error', async () => { + mockEsClient.asInternalUser.search.mockRejectedValue(new Error('ES error')); + + const result = await tool.availability!.handler( + createToolAvailabilityContext(mockRequest, 'default') + ); + + expect(result.status).toBe('unavailable'); + expect(result.reason).toContain('Failed to check Security Labs knowledge base availability'); + }); + }); + + describe('handler', () => { + it('enhances query with Security Labs filter', async () => { + const mockResults = [ + { + type: ToolResultType.resource, + data: { + reference: { id: 'test-id', index: 'test-index' }, + content: { content: 'test result' }, + }, + }, + ]; + (runSearchTool as jest.Mock).mockResolvedValue({ results: mockResults }); + + await tool.handler( + { query: 'test query' }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger, { + modelProvider: mockModelProvider, + events: mockEvents, + }) + ); + + expect(runSearchTool).toHaveBeenCalled(); + const callArgs = (runSearchTool as jest.Mock).mock.calls[0][0]; + expect(callArgs.nlQuery).toContain('test query'); + expect(callArgs.nlQuery).toContain(`kb_resource: ${SECURITY_LABS_RESOURCE}`); + expect(callArgs.nlQuery).toContain('Limit to 3 results'); + }); + + it('calls runSearchTool with correct parameters', async () => { + const mockResults = [ + { + type: ToolResultType.resource, + data: { + reference: { id: 'test-id', index: 'test-index' }, + content: { content: 'test result' }, + }, + }, + ]; + (runSearchTool as jest.Mock).mockResolvedValue({ results: mockResults }); + + await tool.handler( + { query: 'malware analysis' }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger, { + modelProvider: mockModelProvider as ToolHandlerContext['modelProvider'], + events: mockEvents as ToolHandlerContext['events'], + }) + ); + + expect(runSearchTool).toHaveBeenCalledWith({ + nlQuery: expect.stringContaining('malware analysis'), + index: '.kibana-elastic-ai-assistant-knowledge-base-default', + model: { model: 'test-model' }, + esClient: mockEsClient.asCurrentUser, + logger: mockLogger, + events: mockEvents, + }); + }); + + it('handles errors', async () => { + const error = new Error('Search tool error'); + (runSearchTool as jest.Mock).mockRejectedValue(error); + + const result = await tool.handler( + { query: 'test query' }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger, { + modelProvider: mockModelProvider as ToolHandlerContext['modelProvider'], + events: mockEvents as ToolHandlerContext['events'], + }) + ); + + expect(result.results).toHaveLength(1); + const errorResult = result.results[0] as ErrorResult; + expect(errorResult.type).toBe(ToolResultType.error); + expect(errorResult.data.message).toContain('Error: Search tool error'); + expect(mockLogger.error).toHaveBeenCalled(); + }); + }); +}); + From e7e4f2e42d7212f203236908f9d2e748f3e1bc23 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 12:13:12 +0100 Subject: [PATCH 61/96] zIndex --- .../round_thinking/round_flyout.tsx | 10 +++++++++- .../round_thinking/steps/tool_response_flyout.tsx | 11 ++++++++++- .../public/flyout/open_conversation_flyout.tsx | 5 +++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/round_flyout.tsx b/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/round_flyout.tsx index c618879b1116a..af19f2bafe44b 100644 --- a/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/round_flyout.tsx +++ b/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/round_flyout.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiCodeBlock } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { euiThemeVars } from '@kbn/ui-theme'; import type { ConversationRound } from '@kbn/onechat-common'; import { css } from '@emotion/react'; @@ -28,7 +29,14 @@ export const RoundFlyout: React.FC = ({ isOpen, onClose, if (!isOpen) return null; return ( - +

{rawResponseFlyoutTitle}

diff --git a/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/steps/tool_response_flyout.tsx b/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/steps/tool_response_flyout.tsx index 9482f31ad13a8..391cdd68d8b02 100644 --- a/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/steps/tool_response_flyout.tsx +++ b/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/steps/tool_response_flyout.tsx @@ -14,6 +14,8 @@ import { EuiText, EuiSpacer, } from '@elastic/eui'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; const toolResponseFlyoutTitle = i18n.translate( @@ -44,7 +46,14 @@ export const ToolResponseFlyout: React.FC = ({ if (!isOpen) return null; return ( - +

{toolResponseFlyoutTitle}

diff --git a/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx b/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx index 4104d48d70b85..9d91e96fd1863 100644 --- a/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx +++ b/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx @@ -9,6 +9,8 @@ import React, { Suspense, lazy } from 'react'; import type { CoreStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { EuiLoadingSpinner, htmlIdGenerator } from '@elastic/eui'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { css } from '@emotion/react/dist/emotion-react.cjs'; import type { OpenConversationFlyoutOptions } from './types'; import type { OnechatInternalService } from '../services'; import type { ConversationFlyoutRef } from '../types'; @@ -72,6 +74,9 @@ export function openConversationFlyout( hideCloseButton: true, 'aria-labelledby': ariaLabelledBy, maxWidth: 1200, // Maximum width for resizable flyout to prevent NaN error + css: css` + z-index: ${euiThemeVars.euiZFlyout + 3}; + `, } ); From f71074400433674198aa30482276fcf86da4016d Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:29:37 +0000 Subject: [PATCH 62/96] Changes from node scripts/lint_ts_projects --fix --- .../plugins/shared/agent_builder_platform/tsconfig.json | 3 +++ x-pack/platform/plugins/shared/onechat/tsconfig.json | 4 +++- .../security/plugins/security_solution/tsconfig.json | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/x-pack/platform/plugins/shared/agent_builder_platform/tsconfig.json b/x-pack/platform/plugins/shared/agent_builder_platform/tsconfig.json index af879f0528182..a12bbf53a100f 100644 --- a/x-pack/platform/plugins/shared/agent_builder_platform/tsconfig.json +++ b/x-pack/platform/plugins/shared/agent_builder_platform/tsconfig.json @@ -28,5 +28,8 @@ "@kbn/onechat-plugin", "@kbn/core-lifecycle-server", "@kbn/esql-validation-autocomplete", + "@kbn/cases-plugin", + "@kbn/core-http-server", + "@kbn/spaces-plugin", ] } diff --git a/x-pack/platform/plugins/shared/onechat/tsconfig.json b/x-pack/platform/plugins/shared/onechat/tsconfig.json index 7c35ffc3b07bb..e421d744528c7 100644 --- a/x-pack/platform/plugins/shared/onechat/tsconfig.json +++ b/x-pack/platform/plugins/shared/onechat/tsconfig.json @@ -93,6 +93,8 @@ "@kbn/shared-ux-utility", "@kbn/usage-collection-plugin", "@kbn/core-notifications-browser", - "@kbn/deeplinks-agent-builder" + "@kbn/deeplinks-agent-builder", + "@kbn/ui-theme", + "@kbn/cases-plugin" ] } diff --git a/x-pack/solutions/security/plugins/security_solution/tsconfig.json b/x-pack/solutions/security/plugins/security_solution/tsconfig.json index 02bbf2eab49b3..ab6568c18749c 100644 --- a/x-pack/solutions/security/plugins/security_solution/tsconfig.json +++ b/x-pack/solutions/security/plugins/security_solution/tsconfig.json @@ -265,7 +265,7 @@ "@kbn/tour-queue", "@kbn/shared-ux-file-types", "@kbn/onechat-common", - "@kbn/spaces-utils", - "@kbn/onechat-plugin" + "@kbn/onechat-plugin", + "@kbn/core-ui-settings-server-mocks" ] } From 83697e6788fbd5fb8fd8e56841529d0c2d28b6c6 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:50:44 +0000 Subject: [PATCH 63/96] Changes from node scripts/regenerate_moon_projects.js --update --- x-pack/platform/plugins/shared/agent_builder_platform/moon.yml | 3 +++ x-pack/platform/plugins/shared/onechat/moon.yml | 2 ++ x-pack/solutions/security/plugins/security_solution/moon.yml | 3 +++ 3 files changed, 8 insertions(+) diff --git a/x-pack/platform/plugins/shared/agent_builder_platform/moon.yml b/x-pack/platform/plugins/shared/agent_builder_platform/moon.yml index 731b41a04d6fd..4e658010c3f27 100644 --- a/x-pack/platform/plugins/shared/agent_builder_platform/moon.yml +++ b/x-pack/platform/plugins/shared/agent_builder_platform/moon.yml @@ -32,6 +32,9 @@ dependsOn: - '@kbn/onechat-plugin' - '@kbn/core-lifecycle-server' - '@kbn/esql-validation-autocomplete' + - '@kbn/cases-plugin' + - '@kbn/core-http-server' + - '@kbn/spaces-plugin' tags: - plugin - prod diff --git a/x-pack/platform/plugins/shared/onechat/moon.yml b/x-pack/platform/plugins/shared/onechat/moon.yml index faae22443fca9..945e80eaa6584 100644 --- a/x-pack/platform/plugins/shared/onechat/moon.yml +++ b/x-pack/platform/plugins/shared/onechat/moon.yml @@ -98,6 +98,8 @@ dependsOn: - '@kbn/usage-collection-plugin' - '@kbn/core-notifications-browser' - '@kbn/deeplinks-agent-builder' + - '@kbn/ui-theme' + - '@kbn/cases-plugin' tags: - plugin - prod diff --git a/x-pack/solutions/security/plugins/security_solution/moon.yml b/x-pack/solutions/security/plugins/security_solution/moon.yml index fb67d490d0675..5d6063bc8d1a3 100644 --- a/x-pack/solutions/security/plugins/security_solution/moon.yml +++ b/x-pack/solutions/security/plugins/security_solution/moon.yml @@ -263,6 +263,9 @@ dependsOn: - '@kbn/connector-schemas' - '@kbn/tour-queue' - '@kbn/shared-ux-file-types' + - '@kbn/onechat-common' + - '@kbn/onechat-plugin' + - '@kbn/core-ui-settings-server-mocks' tags: - plugin - prod From 95afa72200c3f572124821f230ff5f9f9ad8264d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 12:59:49 +0100 Subject: [PATCH 64/96] fix import --- .../shared/onechat/public/flyout/open_conversation_flyout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx b/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx index 9d91e96fd1863..149856424951f 100644 --- a/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx +++ b/x-pack/platform/plugins/shared/onechat/public/flyout/open_conversation_flyout.tsx @@ -10,7 +10,7 @@ import type { CoreStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { EuiLoadingSpinner, htmlIdGenerator } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; -import { css } from '@emotion/react/dist/emotion-react.cjs'; +import { css } from '@emotion/react'; import type { OpenConversationFlyoutOptions } from './types'; import type { OnechatInternalService } from '../services'; import type { ConversationFlyoutRef } from '../types'; From 6a3aecdab4fc75e927e96717abf7e28ebf0cdb9d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 13:25:34 +0100 Subject: [PATCH 65/96] use real platformCoreTools.productDocumentation tool --- .../server/attachment_types/product_reference.ts | 4 ++-- .../server/agent_builder/attachments/query_help.ts | 3 +-- .../server/agent_builder/tools/register_tools.ts | 6 +----- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts b/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts index 56433a7d59660..9159a177e06cf 100644 --- a/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts +++ b/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts @@ -11,6 +11,7 @@ import { productReferenceAttachmentDataSchema, } from '@kbn/onechat-common/attachments'; import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import { platformCoreTools } from '@kbn/onechat-common'; /** * Creates the definition for the `product_reference` attachment type. @@ -36,8 +37,7 @@ export const createProductReferenceAttachmentType = (): AttachmentTypeDefinition }, }; }, - // TODO use real tool once https://github.com/elastic/kibana/pull/242598 merges, same in description below - getTools: () => [`platformCoreTools.productDocumentation`], + getTools: () => [platformCoreTools.productDocumentation], getAgentDescription: () => { const description = `You have access to a product reference that needs to be queried for documentation. diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts index 7c3b5009c5d33..1432671bf9b8f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts @@ -50,8 +50,7 @@ export const createQueryHelpAttachmentType = (): AttachmentTypeDefinition => { getTools: () => { const tools: string[] = [ platformCoreTools.generateEsql, - // TODO use real tool once product_documentation tool is merged, same in description below - 'platformCoreTools.productDocumentation', + platformCoreTools.productDocumentation, ]; return tools; }, diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts index 206e281640220..22d2431a34b00 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts @@ -20,17 +20,13 @@ const PLATFORM_TOOL_IDS = [ platformCoreTools.listIndices, platformCoreTools.getIndexMapping, platformCoreTools.getDocumentById, - // TODO add once product doc tool is merged https://github.com/elastic/kibana/pull/242598 - // platformCoreTools.productDocumentation, + platformCoreTools.productDocumentation, ]; export const SECURITY_TOOL_IDS = [ SECURITY_LABS_SEARCH_TOOL_ID, SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, SECURITY_ENTITY_RISK_SCORE_TOOL_ID, ]; - -export const SECURITY_AGENT_TOOL_IDS = [...PLATFORM_TOOL_IDS, ...SECURITY_TOOL_IDS]; - /** * Registers all security agent builder tools with the onechat plugin */ From 97797ab4a40e5586fa750425e0444eea37996358 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 2 Dec 2025 12:54:47 +0000 Subject: [PATCH 66/96] Changes from node scripts/eslint_all_files --no-cache --fix --- .../plugins/shared/onechat/server/utils/case_urls.ts | 3 +-- .../components/new_agent_builder_attachment.test.tsx | 1 - .../components/new_agent_builder_attachment.tsx | 1 - .../public/agent_builder/helpers.test.tsx | 9 ++++----- .../hooks/use_agent_builder_attachment.test.tsx | 1 - .../public/agent_builder/jest.config.js | 1 - .../server/agent_builder/attachments/alert.test.ts | 1 - .../server/agent_builder/attachments/entity.test.ts | 1 - .../server/agent_builder/attachments/query_help.test.ts | 1 - .../server/agent_builder/jest.config.js | 1 - .../tools/attack_discovery_search_tool.test.ts | 9 ++++++--- .../agent_builder/tools/entity_risk_score_tool.test.ts | 1 - .../server/agent_builder/tools/helpers.test.ts | 1 - .../server/agent_builder/tools/helpers.ts | 1 - .../tools/security_labs_search_tool.test.ts | 9 +++++++-- 15 files changed, 18 insertions(+), 23 deletions(-) diff --git a/x-pack/platform/plugins/shared/onechat/server/utils/case_urls.ts b/x-pack/platform/plugins/shared/onechat/server/utils/case_urls.ts index fd789441d02a2..6faf7d23d0b0a 100644 --- a/x-pack/platform/plugins/shared/onechat/server/utils/case_urls.ts +++ b/x-pack/platform/plugins/shared/onechat/server/utils/case_urls.ts @@ -9,8 +9,8 @@ import type { KibanaRequest } from '@kbn/core-http-server'; import type { CoreStart } from '@kbn/core/server'; import { getCaseViewPath } from '@kbn/cases-plugin/server/common/utils'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; -import { getCurrentSpaceId } from './spaces'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; +import { getCurrentSpaceId } from './spaces'; /** * App routes for different Kibana applications @@ -92,4 +92,3 @@ export function getCaseUrl( return null; } } - diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.test.tsx index 291b561b5b452..2aacef80427e6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.test.tsx @@ -109,4 +109,3 @@ describe('NewAgentBuilderAttachment', () => { expect(screen.getByTestId('newAgentBuilderAttachment')).toBeInTheDocument(); }); }); - diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx index f1c3668e22154..024d6b6e5eb3c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx @@ -67,4 +67,3 @@ NewAgentBuilderAttachmentComponent.displayName = 'NewAgentBuilderAttachmentCompo * with attachment data. You may optionally override the default text. */ export const NewAgentBuilderAttachment = React.memo(NewAgentBuilderAttachmentComponent); - diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx index 2b04cc8f18c0c..0729f62ca1f84 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx @@ -13,8 +13,8 @@ describe('filterAndStringifyAlertData', () => { const rawData: Record = { [ESSENTIAL_ALERT_FIELDS[0]]: ['value1'], [ESSENTIAL_ALERT_FIELDS[1]]: ['value2'], - 'nonEssentialField': ['shouldBeExcluded'], - 'anotherNonEssential': ['shouldAlsoBeExcluded'], + nonEssentialField: ['shouldBeExcluded'], + anotherNonEssential: ['shouldAlsoBeExcluded'], }; const result = filterAndStringifyAlertData(rawData); @@ -28,8 +28,8 @@ describe('filterAndStringifyAlertData', () => { it('excludes non-essential fields', () => { const rawData: Record = { - 'field1': ['value1'], - 'field2': ['value2'], + field1: ['value1'], + field2: ['value2'], }; const result = filterAndStringifyAlertData(rawData); @@ -60,4 +60,3 @@ describe('filterAndStringifyAlertData', () => { expect(parsed).toEqual({}); }); }); - diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx index b76e5d7d30cdf..e7d2d4f5ac7e3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx @@ -156,4 +156,3 @@ describe('useAgentBuilderAttachment', () => { expect(content).toEqual({ alert: 'test alert data' }); }); }); - diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/jest.config.js b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/jest.config.js index 6d301f21ad66f..8f60c49239d2c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/jest.config.js +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/jest.config.js @@ -17,4 +17,3 @@ module.exports = { ], moduleNameMapper: require('../../server/__mocks__/module_name_map'), }; - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.test.ts index cf005b6ce9c49..517540fca7e22 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/alert.test.ts @@ -108,4 +108,3 @@ describe('createAlertAttachmentType', () => { }); }); }); - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity.test.ts index f579367ce3c8b..eefcf0403ef18 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/entity.test.ts @@ -153,4 +153,3 @@ describe('createEntityAttachmentType', () => { }); }); }); - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts index ae04f02b8f781..aa449ce022faf 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts @@ -111,4 +111,3 @@ describe('createQueryHelpAttachmentType', () => { }); }); }); - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js index a6a38d771b361..f8b206cc154a0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js @@ -17,4 +17,3 @@ module.exports = { ], moduleNameMapper: require('../__mocks__/module_name_map'), }; - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts index b6f96741e4561..66e08a58d0921 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts @@ -76,8 +76,12 @@ describe('attackDiscoverySearchTool', () => { expect(executeEsql).toHaveBeenCalled(); const callArgs = (executeEsql as jest.Mock).mock.calls[0][0]; expect(callArgs.query).toContain('FROM .alerts-security.attack.discovery.alerts-default*'); - expect(callArgs.query).toContain('MV_CONTAINS(kibana.alert.attack_discovery.alert_ids,"alert-1")'); - expect(callArgs.query).toContain('MV_CONTAINS(kibana.alert.attack_discovery.alert_ids,"alert-2")'); + expect(callArgs.query).toContain( + 'MV_CONTAINS(kibana.alert.attack_discovery.alert_ids,"alert-1")' + ); + expect(callArgs.query).toContain( + 'MV_CONTAINS(kibana.alert.attack_discovery.alert_ids,"alert-2")' + ); expect(callArgs.query).toContain('@timestamp >='); expect(callArgs.query).toContain('LIMIT 100'); }); @@ -159,4 +163,3 @@ describe('attackDiscoverySearchTool', () => { }); }); }); - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts index 46fdb0bd295fa..0402c57242c53 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts @@ -237,4 +237,3 @@ describe('entityRiskScoreTool', () => { }); }); }); - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts index d18529d9d3737..17d784638daba 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts @@ -40,4 +40,3 @@ describe('getSpaceIdFromRequest', () => { expect(spaceId).toBe(DEFAULT_SPACE_ID); }); }); - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.ts index 2f869a34027f6..eaa86f0fe8ebb 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.ts @@ -17,4 +17,3 @@ export const getSpaceIdFromRequest = (request: KibanaRequest): string => { const { spaceId } = getSpaceIdFromPath(pathname); return spaceId ?? DEFAULT_SPACE_ID; }; - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.test.ts index 2c8e47f123d5b..efe9dac6e8f80 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/security_labs_search_tool.test.ts @@ -75,7 +75,13 @@ describe('securityLabsSearchTool', () => { timed_out: false, _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, hits: { - hits: [{ _id: 'test-id', _index: 'test-index', _source: { kb_resource: SECURITY_LABS_RESOURCE } }], + hits: [ + { + _id: 'test-id', + _index: 'test-index', + _source: { kb_resource: SECURITY_LABS_RESOURCE }, + }, + ], total: { value: 1, relation: 'eq' }, }, }); @@ -212,4 +218,3 @@ describe('securityLabsSearchTool', () => { }); }); }); - From 276350369829bd3fe8ac878fc845427d5fa3052e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 14:00:07 +0100 Subject: [PATCH 67/96] fixing --- .../agent_builder/components/prompts.ts | 114 +++++++++--------- .../flyout/document_details/right/footer.tsx | 7 +- .../agent_builder/tools/register_tools.ts | 23 +--- 3 files changed, 66 insertions(+), 78 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts index fa6fc1ddd3f5f..021c3a6ff978e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts @@ -8,74 +8,76 @@ export const ATTACK_DISCOVERY_ATTACHMENT_PROMPT = `Summarize the attack discovery attached and recommend next steps. Find the risk score for each extracted host.name and user.name. Case URLs MUST be included in the response if they exist. Summary should be in markdown.`; export const ALERT_ATTACHMENT_PROMPT = `Evaluate the provided security alert and generate a structured, markdown-formatted summary suitable for inclusion in an Elastic Security case. Use all available enrichment tools before generating your response. Include the following sections: ---- +1. Event Description 📝 + - Summarize the alert using extracted data: + * **Alert ID**: Use the \`_id\` field value (not \`kibana.alert.uuid\`) + * **Rule Name**: \`kibana.alert.rule.name\` + * **Entities**: \`host.name\`, \`user.name\`, \`service.name\` + * Include associated **risk scores** for each entity (from the risk score tool) + * Reference **MITRE ATT&CK techniques** with links (\`kibana.alert.rule.threat.technique.id\`, \`kibana.alert.rule.threat.tactic.id\`, \`threat.tactic.id\`) -## 1. Event Description 📝 +2. Associated Cases & Attack Discoveries 🔍 + - Summarize any attack discoveries that include this alert ID, highlighting: + * Involved hosts, users, and status + * Patterns or recurring behaviors + - List all open or related security cases referencing this alert ID, **always using markdown links** to the case URLs (from the cases tool) -* Summarize the alert using extracted data: +3. Triage Steps 🛡️ - * **Alert ID**: Use the \`_id\` field value (not \`kibana.alert.uuid\`) - * **Rule Name**: \`kibana.alert.rule.name\` - * **Entities**: \`host.name\`, \`user.name\`, \`service.name\` - * Include associated **risk scores** for each entity (from the risk score tool) - * Reference **MITRE ATT&CK techniques** with links (\`kibana.alert.rule.threat.technique.id\`, \`kibana.alert.rule.threat.tactic.id\`, \`threat.tactic.id\`) + - Provide clear, actionable triage steps tailored to Elastic Security workflows: + * Consider the alert’s rule, involved entities, and MITRE context + * Include relevant detection rules or anomaly findings + * Reference Security Labs articles related to the MITRE technique or alert rule (with links) ---- +4. Recommended Actions ⚡ -## 2. Associated Cases & Attack Discoveries 🔍 + - Prioritized response actions using enriched context: + * **Elastic Defend endpoint actions** (e.g., isolate host, kill process, retrieve/delete file) with documentation links + * **Example queries for further investigation**: + * ESQL queries (code blocks) + * OSQuery Manager queries (code blocks) + - Guidance for using **Timelines** and **Entity Analytics** for deeper context (with documentation links) -* Summarize any attack discoveries that include this alert ID, highlighting: +5. MITRE ATT&CK Context 📊 - * Involved hosts, users, and status - * Patterns or recurring behaviors + - Summarize mapped MITRE ATT&CK techniques + - Provide actionable recommendations based on MITRE guidance, including hyperlinks -* List all open or related security cases referencing this alert ID, **always using markdown links** to the case URLs (from the cases tool) +6. Documentation Links 📚 ---- - -## 3. Triage Steps 🛡️ - -* Provide clear, actionable triage steps tailored to Elastic Security workflows: - - * Consider the alert’s rule, involved entities, and MITRE context - * Include relevant detection rules or anomaly findings - * Reference Security Labs articles related to the MITRE technique or alert rule (with links) - ---- - -## 4. Recommended Actions ⚡ - -* Prioritized response actions using enriched context: - - * **Elastic Defend endpoint actions** (e.g., isolate host, kill process, retrieve/delete file) with documentation links - * **Example queries for further investigation**: - - * Elasticsearch / EQL queries (code blocks) - * OSQuery Manager queries (code blocks) - - * Guidance for using **Timelines** and **Entity Analytics** for deeper context (with documentation links) - ---- - -## 5. MITRE ATT&CK Context 📊 - -* Summarize mapped MITRE ATT&CK techniques -* Provide actionable recommendations based on MITRE guidance, including hyperlinks - ---- - -## 6. Documentation Links 📚 - -* Include direct links to all referenced Elastic Security documentation, Security Labs articles, and MITRE ATT&CK pages - ---- + - Include direct links to all referenced Elastic Security documentation, Security Labs articles, and MITRE ATT&CK pages **Formatting Requirements** -* Use markdown headers, tables, and code blocks for clarity -* Organize sections visually and consistently -* Use concise, actionable language -* Include emojis in section headers for clarity`; + - Use markdown headers, tables, and code blocks for clarity + - Organize sections visually and consistently + - Use concise, actionable language + - Include emojis in section headers for clarity`; +export const EVENT_ATTACHMENT_PROMPT = `Evaluate the security event described above and provide a structured, markdown-formatted summary suitable for inclusion in an Elastic Security case. Ensure you're using all tools available to you. Your response must include: +1. Event Description + - Summarize the event using extracted data: + * **Entities**: \`host.name\`, \`user.name\`, \`service.name\` + * Include associated **risk scores** for each entity (from the risk score tool) + - Reference relevant MITRE ATT&CK techniques, with hyperlinks to the official MITRE pages. +2. Triage Steps + - List clear, bulleted triage steps tailored to Elastic Security workflows (e.g., alert investigation, timeline creation, entity analytics review). + - Highlight any relevant detection rules or anomaly findings. +3. Recommended Actions + - Provide prioritized response actions, including: + - Elastic Defend endpoint response actions (e.g., isolate host, kill process, retrieve/delete file), with links to Elastic documentation. + - Example ES|QL queries for further investigation, formatted as code blocks. + - Example OSQuery Manager queries for further investigation, formatted as code blocks. + - Guidance on using Timelines and Entity Analytics for deeper context, with documentation links. +4. MITRE ATT&CK Context + - Summarize the mapped MITRE ATT&CK techniques and provide actionable recommendations based on MITRE guidance, with hyperlinks. +5. Documentation Links + - Include direct links to all referenced Elastic Security documentation and MITRE ATT&CK pages. +Formatting Requirements: + - Use markdown headers, tables, and code blocks for clarity. + - Organize the response into visually distinct sections. + - Use concise, actionable language. + - Include relevant emojis in section headers for visual clarity (e.g., 📝, 🛡️, 🔍, 📚).`; + export const ENTITY_ANALYSIS = `Analyze asset data described above to provide security insights. The data contains the context of a specific asset (e.g., a host, user, service or cloud resource). Your response must be structured, contextual, and provide a general analysis based on the structure below. Your response must be in markdown format and include the following sections: **1. 🔍 Asset Overview** diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx index bfcfe5c3f0992..2af0b79ecab1f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx @@ -10,7 +10,10 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NewChatByTitle } from '@kbn/elastic-assistant'; -import { ALERT_ATTACHMENT_PROMPT } from '../../../agent_builder/components/prompts'; +import { + ALERT_ATTACHMENT_PROMPT, + EVENT_ATTACHMENT_PROMPT, +} from '../../../agent_builder/components/prompts'; import { useBasicDataFromDetailsData } from '../shared/hooks/use_basic_data_from_details_data'; import { useDocumentDetailsContext } from '../shared/context'; import { useAssistant } from './hooks/use_assistant'; @@ -57,7 +60,7 @@ export const PanelFooter: FC = ({ isRulePreview }) => { const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ attachmentType: SecurityAgentBuilderAttachments.alert, attachmentData: { alert: alertData }, - attachmentPrompt: ALERT_ATTACHMENT_PROMPT, + attachmentPrompt: isAlert ? ALERT_ATTACHMENT_PROMPT : EVENT_ATTACHMENT_PROMPT, }); if (isRulePreview) return null; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts index 22d2431a34b00..b0c03fb7675ca 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts @@ -5,28 +5,11 @@ * 2.0. */ -import { platformCoreTools } from '@kbn/onechat-common'; import type { OnechatPluginSetup } from '@kbn/onechat-plugin/server'; import type { CoreSetup } from '@kbn/core-lifecycle-server'; -import { SECURITY_LABS_SEARCH_TOOL_ID, securityLabsSearchTool } from './security_labs_search_tool'; -import { - attackDiscoverySearchTool, - SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, -} from './attack_discovery_search_tool'; -import { entityRiskScoreTool, SECURITY_ENTITY_RISK_SCORE_TOOL_ID } from './entity_risk_score_tool'; - -const PLATFORM_TOOL_IDS = [ - platformCoreTools.search, - platformCoreTools.listIndices, - platformCoreTools.getIndexMapping, - platformCoreTools.getDocumentById, - platformCoreTools.productDocumentation, -]; -export const SECURITY_TOOL_IDS = [ - SECURITY_LABS_SEARCH_TOOL_ID, - SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, - SECURITY_ENTITY_RISK_SCORE_TOOL_ID, -]; +import { securityLabsSearchTool } from './security_labs_search_tool'; +import { attackDiscoverySearchTool } from './attack_discovery_search_tool'; +import { entityRiskScoreTool } from './entity_risk_score_tool'; /** * Registers all security agent builder tools with the onechat plugin */ From 3f9567362c6e274eff1b1322dbb9499f769a4d07 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 14:35:33 +0100 Subject: [PATCH 68/96] new agent builder dirs to CODEOWNERS --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index afad20c4dc4f4..1515f42626927 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2477,6 +2477,8 @@ x-pack/platform/test/functional/page_objects/search_profiler_page.ts @elastic/se /x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/automatic_import @elastic/integration-experience /x-pack/solutions/security/plugins/security_solution/public/configurations @elastic/security-generative-ai /x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_soc @elastic/security-solution @elastic/security-threat-hunting-investigations +/x-pack/solutions/security/plugins/security_solution/public/agent_builder @elastic/security-generative-ai +/x-pack/solutions/security/plugins/security_solution/server/agent_builder @elastic/security-generative-ai # AI4DSOC in Security Solution /x-pack/solutions/security/test/security_solution_cypress/cypress/e2e/ai4dsoc @elastic/security-engineering-productivity From 0ca2a14ff9c871cf062299d6baa43e5e78a0958e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 14:42:06 +0100 Subject: [PATCH 69/96] ownFocus={false} --- .../conversation_rounds/round_thinking/round_flyout.tsx | 1 + .../round_thinking/steps/tool_response_flyout.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/round_flyout.tsx b/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/round_flyout.tsx index af19f2bafe44b..b0768e86d0f51 100644 --- a/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/round_flyout.tsx +++ b/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/round_flyout.tsx @@ -33,6 +33,7 @@ export const RoundFlyout: React.FC = ({ isOpen, onClose, onClose={onClose} aria-labelledby="rawResponseFlyoutTitle" size="m" + ownFocus={false} css={css` z-index: ${euiThemeVars.euiZFlyout + 4}; `} diff --git a/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/steps/tool_response_flyout.tsx b/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/steps/tool_response_flyout.tsx index 391cdd68d8b02..eb2d2669ce53d 100644 --- a/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/steps/tool_response_flyout.tsx +++ b/x-pack/platform/plugins/shared/onechat/public/application/components/conversations/conversation_rounds/round_thinking/steps/tool_response_flyout.tsx @@ -50,6 +50,7 @@ export const ToolResponseFlyout: React.FC = ({ onClose={onClose} aria-labelledby="toolResponseFlyoutTitle" size="m" + ownFocus={false} css={css` z-index: ${euiThemeVars.euiZFlyout + 4}; `} From cf2090a72346026945fc5ca25041933848e730cf Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 15:33:34 +0100 Subject: [PATCH 70/96] fix lint --- .../hooks/use_agent_builder_attachment.test.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx index e7d2d4f5ac7e3..b514b41aa5455 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx @@ -10,24 +10,26 @@ import React from 'react'; import { TestProviders } from '../../common/mock'; import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; import { useAgentBuilderAttachment } from './use_agent_builder_attachment'; +import type { OnechatPluginStart } from '@kbn/onechat-plugin/public'; const mockOpenConversationFlyout = jest.fn(); -const createWrapper = (onechatService: typeof mockOnechatService | null = mockOnechatService) => { +const createWrapper = (onechatService?: OnechatPluginStart) => { const mockStartServices = createStartServicesMock(); const startServices = { ...mockStartServices, onechat: onechatService ?? undefined, }; + // eslint-disable-next-line react/display-name return ({ children }: { children: React.ReactNode }) => ( {children} ); }; -const mockOnechatService = { +const mockOnechatService: OnechatPluginStart = { openConversationFlyout: mockOpenConversationFlyout, - tools: {} as any, + tools: {} as OnechatPluginStart['tools'], setConversationFlyoutActiveConfig: jest.fn(), clearConversationFlyoutActiveConfig: jest.fn(), }; @@ -99,7 +101,7 @@ describe('useAgentBuilderAttachment', () => { it('handles missing onechat service gracefully', () => { const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { - wrapper: createWrapper(null), + wrapper: createWrapper(), }); act(() => { From 12d57230215da6c8222a876ca4ffbe720ce63457 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 16:09:35 +0100 Subject: [PATCH 71/96] fixes --- .../use_agent_builder_attachment.test.tsx | 37 +++++++++++++------ .../tools/entity_risk_score_tool.test.ts | 3 +- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx index b514b41aa5455..d290e812588c1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx @@ -10,9 +10,18 @@ import React from 'react'; import { TestProviders } from '../../common/mock'; import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; import { useAgentBuilderAttachment } from './use_agent_builder_attachment'; -import type { OnechatPluginStart } from '@kbn/onechat-plugin/public'; +import type { OnechatPluginStart, OpenConversationFlyoutReturn } from '@kbn/onechat-plugin/public'; -const mockOpenConversationFlyout = jest.fn(); +const mockFlyoutRef = { + close: jest.fn(), +}; + +const mockOpenConversationFlyout = jest.fn< + OpenConversationFlyoutReturn, + Parameters +>(() => ({ + flyoutRef: mockFlyoutRef, +})); const createWrapper = (onechatService?: OnechatPluginStart) => { const mockStartServices = createStartServicesMock(); @@ -52,7 +61,7 @@ describe('useAgentBuilderAttachment', () => { it('returns openAgentBuilderFlyout function', () => { const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { - wrapper: createWrapper(), + wrapper: createWrapper(mockOnechatService), }); expect(result.current.openAgentBuilderFlyout).toBeDefined(); @@ -61,7 +70,7 @@ describe('useAgentBuilderAttachment', () => { it('opens flyout with correct attachment data and prompt', () => { const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { - wrapper: createWrapper(), + wrapper: createWrapper(mockOnechatService), }); act(() => { @@ -85,7 +94,7 @@ describe('useAgentBuilderAttachment', () => { it('opens flyout with correct sessionTag', () => { const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { - wrapper: createWrapper(), + wrapper: createWrapper(mockOnechatService), }); act(() => { @@ -112,11 +121,17 @@ describe('useAgentBuilderAttachment', () => { }); it('handles missing openConversationFlyout method gracefully', () => { + const partialOnechatService: Partial & + Pick< + OnechatPluginStart, + 'tools' | 'setConversationFlyoutActiveConfig' | 'clearConversationFlyoutActiveConfig' + > = { + ...mockOnechatService, + openConversationFlyout: undefined, + }; + const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { - wrapper: createWrapper({ - ...mockOnechatService, - openConversationFlyout: undefined, - } as Partial as typeof mockOnechatService), + wrapper: createWrapper(partialOnechatService as OnechatPluginStart), }); act(() => { @@ -128,7 +143,7 @@ describe('useAgentBuilderAttachment', () => { it('generates attachment ID with timestamp', async () => { const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { - wrapper: createWrapper(), + wrapper: createWrapper(mockOnechatService), }); act(() => { @@ -144,7 +159,7 @@ describe('useAgentBuilderAttachment', () => { it('attachment getContent returns correct data', async () => { const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { - wrapper: createWrapper(), + wrapper: createWrapper(mockOnechatService), }); act(() => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts index 0402c57242c53..7ad665aa8d00e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts @@ -15,6 +15,7 @@ import { setupMockCoreStartServices, } from '../__mocks__/test_helpers'; import { entityRiskScoreTool } from './entity_risk_score_tool'; +import { createGetRiskScores } from '../../lib/entity_analytics/risk_score/get_risk_score'; jest.mock('../../lib/entity_analytics/risk_score/get_risk_score', () => ({ createGetRiskScores: jest.fn(() => jest.fn()), @@ -102,8 +103,6 @@ describe('entityRiskScoreTool', () => { }); describe('handler', () => { - const { createGetRiskScores } = require('../../lib/entity_analytics/risk_score/get_risk_score'); - beforeEach(() => { createGetRiskScores.mockReturnValue(jest.fn()); }); From 8f7807f9e1293bc6c7c236383adee9afffd05d8e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 16:29:24 +0100 Subject: [PATCH 72/96] more fixies --- .../hooks/use_agent_builder_attachment.test.tsx | 13 ++++++++----- .../agent_builder/attachments/query_help.test.ts | 2 +- .../tools/entity_risk_score_tool.test.ts | 16 ++++++++-------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx index d290e812588c1..40f215ecda168 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx @@ -10,14 +10,14 @@ import React from 'react'; import { TestProviders } from '../../common/mock'; import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock'; import { useAgentBuilderAttachment } from './use_agent_builder_attachment'; -import type { OnechatPluginStart, OpenConversationFlyoutReturn } from '@kbn/onechat-plugin/public'; +import type { OnechatPluginStart } from '@kbn/onechat-plugin/public'; const mockFlyoutRef = { close: jest.fn(), }; const mockOpenConversationFlyout = jest.fn< - OpenConversationFlyoutReturn, + unknown, Parameters >(() => ({ flyoutRef: mockFlyoutRef, @@ -37,7 +37,8 @@ const createWrapper = (onechatService?: OnechatPluginStart) => { }; const mockOnechatService: OnechatPluginStart = { - openConversationFlyout: mockOpenConversationFlyout, + openConversationFlyout: + mockOpenConversationFlyout as OnechatPluginStart['openConversationFlyout'], tools: {} as OnechatPluginStart['tools'], setConversationFlyoutActiveConfig: jest.fn(), clearConversationFlyoutActiveConfig: jest.fn(), @@ -151,7 +152,7 @@ describe('useAgentBuilderAttachment', () => { }); const callArgs = mockOpenConversationFlyout.mock.calls[0][0]; - const attachment = callArgs.attachments[0]; + const attachment = callArgs?.attachments?.length ? callArgs?.attachments[0] : { id: '' }; expect(attachment.id).toBe('alert-1234567890'); expect(attachment.id).toMatch(/^alert-\d+$/); @@ -167,7 +168,9 @@ describe('useAgentBuilderAttachment', () => { }); const callArgs = mockOpenConversationFlyout.mock.calls[0][0]; - const attachment = callArgs.attachments[0]; + const attachment = callArgs?.attachments?.length + ? callArgs?.attachments[0] + : { getContent: jest.fn() }; const content = await attachment.getContent(); expect(content).toEqual({ alert: 'test alert data' }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts index aa449ce022faf..475c0db225a5f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts @@ -95,7 +95,7 @@ describe('createQueryHelpAttachmentType', () => { expect(tools).toBeDefined(); if (tools) { expect(tools).toContain(platformCoreTools.generateEsql); - expect(tools).toContain('platformCoreTools.productDocumentation'); + expect(tools).toContain(platformCoreTools.productDocumentation); } }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts index 7ad665aa8d00e..ea90f63e128dd 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts @@ -17,9 +17,9 @@ import { import { entityRiskScoreTool } from './entity_risk_score_tool'; import { createGetRiskScores } from '../../lib/entity_analytics/risk_score/get_risk_score'; -jest.mock('../../lib/entity_analytics/risk_score/get_risk_score', () => ({ - createGetRiskScores: jest.fn(() => jest.fn()), -})); +jest.mock('../../lib/entity_analytics/risk_score/get_risk_score'); + +const mockCreateGetRiskScores = createGetRiskScores as jest.Mock; describe('entityRiskScoreTool', () => { const { mockCore, mockLogger, mockEsClient, mockRequest } = createToolTestMocks(); @@ -104,7 +104,7 @@ describe('entityRiskScoreTool', () => { describe('handler', () => { beforeEach(() => { - createGetRiskScores.mockReturnValue(jest.fn()); + mockCreateGetRiskScores.mockReturnValue(jest.fn()); }); it('successfully fetches risk score with valid identifierType and identifier', async () => { @@ -122,7 +122,7 @@ describe('entityRiskScoreTool', () => { ], }, ]); - createGetRiskScores.mockReturnValue(mockGetRiskScores); + mockCreateGetRiskScores.mockReturnValue(mockGetRiskScores); mockEsClient.asCurrentUser.search.mockResolvedValue({ took: 1, @@ -159,7 +159,7 @@ describe('entityRiskScoreTool', () => { it('returns error when no risk score found', async () => { const mockGetRiskScores = jest.fn().mockResolvedValue([]); - createGetRiskScores.mockReturnValue(mockGetRiskScores); + mockCreateGetRiskScores.mockReturnValue(mockGetRiskScores); const result = await tool.handler( { identifierType: 'user', identifier: 'username-1' }, @@ -187,7 +187,7 @@ describe('entityRiskScoreTool', () => { ], }, ]); - createGetRiskScores.mockReturnValue(mockGetRiskScores); + mockCreateGetRiskScores.mockReturnValue(mockGetRiskScores); const alertData = { 'kibana.alert.rule.name': 'Test Rule' }; mockEsClient.asCurrentUser.search.mockResolvedValue({ @@ -221,7 +221,7 @@ describe('entityRiskScoreTool', () => { it('handles ES client failures', async () => { const mockGetRiskScores = jest.fn().mockRejectedValue(new Error('ES error')); - createGetRiskScores.mockReturnValue(mockGetRiskScores); + mockCreateGetRiskScores.mockReturnValue(mockGetRiskScores); const result = await tool.handler( { identifierType: 'host', identifier: 'hostname-1' }, From de6820857a9ea509b1a892b3a8b1b0c0c9cc53d5 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Dec 2025 17:30:34 +0100 Subject: [PATCH 73/96] fixing --- .../use_agent_builder_attachment.test.tsx | 20 +------------------ .../hooks/use_agent_builder_attachment.ts | 6 +++--- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx index 40f215ecda168..a865c7f0d9826 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx @@ -86,7 +86,7 @@ describe('useAgentBuilderAttachment', () => { { id: 'alert-1234567890', type: 'alert', - getContent: expect.any(Function), + alert: 'test alert data', }, ], sessionTag: 'security', @@ -157,22 +157,4 @@ describe('useAgentBuilderAttachment', () => { expect(attachment.id).toBe('alert-1234567890'); expect(attachment.id).toMatch(/^alert-\d+$/); }); - - it('attachment getContent returns correct data', async () => { - const { result } = renderHook(() => useAgentBuilderAttachment(defaultParams), { - wrapper: createWrapper(mockOnechatService), - }); - - act(() => { - result.current.openAgentBuilderFlyout(); - }); - - const callArgs = mockOpenConversationFlyout.mock.calls[0][0]; - const attachment = callArgs?.attachments?.length - ? callArgs?.attachments[0] - : { getContent: jest.fn() }; - const content = await attachment.getContent(); - - expect(content).toEqual({ alert: 'test alert data' }); - }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts index d39c09767050b..2b1e758be97cd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.ts @@ -6,7 +6,7 @@ */ import { useCallback } from 'react'; -import type { UiAttachment } from '@kbn/onechat-plugin/public/embeddable/types'; +import type { AttachmentInput } from '@kbn/onechat-common/attachments'; import { useKibana } from '../../common/lib/kibana/use_kibana'; export interface UseAgentBuilderAttachmentParams { @@ -51,10 +51,10 @@ export const useAgentBuilderAttachment = ({ const attachmentId = `${attachmentType}-${Date.now()}`; // Create the UiAttachment object - const attachment: UiAttachment = { + const attachment: AttachmentInput = { id: attachmentId, type: attachmentType, - getContent: async () => attachmentData, + data: attachmentData, }; // Open the conversation flyout with attachment and prefilled message From d8d5013ffe5c36b08c253feb55a47392c1a29f8e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 08:29:10 +0100 Subject: [PATCH 74/96] rm unused file --- .../shared/onechat/server/utils/case_urls.ts | 94 ------------------- 1 file changed, 94 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/onechat/server/utils/case_urls.ts diff --git a/x-pack/platform/plugins/shared/onechat/server/utils/case_urls.ts b/x-pack/platform/plugins/shared/onechat/server/utils/case_urls.ts deleted file mode 100644 index 6faf7d23d0b0a..0000000000000 --- a/x-pack/platform/plugins/shared/onechat/server/utils/case_urls.ts +++ /dev/null @@ -1,94 +0,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 Elastic License - * 2.0. - */ - -import type { KibanaRequest } from '@kbn/core-http-server'; -import type { CoreStart } from '@kbn/core/server'; -import { getCaseViewPath } from '@kbn/cases-plugin/server/common/utils'; -import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; -import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; -import { getCurrentSpaceId } from './spaces'; - -/** - * App routes for different Kibana applications - */ -const APP_ROUTES = { - security: '/app/security', - observability: '/app/observability', - management: '/app/management/insightsAndAlerting', -} as const; - -/** - * Get the app route based on owner/case type - */ -function getAppRoute(owner: string): string { - const ownerToRoute: Record = { - securitySolution: APP_ROUTES.security, - observability: APP_ROUTES.observability, - cases: APP_ROUTES.management, - }; - return ownerToRoute[owner] || APP_ROUTES.management; -} - -/** - * Build a full URL from base components - */ -function buildFullUrl( - request: KibanaRequest, - core: CoreStart, - spaceId: string, - path: string -): string { - const publicBaseUrl = core.http.basePath.publicBaseUrl; - const serverBasePath = core.http.basePath.serverBasePath; - - // First try using publicBaseUrl if configured - if (publicBaseUrl) { - const pathWithSpace = addSpaceIdToPath(serverBasePath, spaceId, path); - return `${publicBaseUrl}${pathWithSpace}`; - } - - // Fallback: construct URL from request - const protocol = request.headers['x-forwarded-proto'] || 'http'; - const host = request.headers.host || 'localhost:5601'; - const baseUrl = `${protocol}://${host}`; - const pathWithSpace = addSpaceIdToPath(serverBasePath, spaceId, path); - - return `${baseUrl}${pathWithSpace}`; -} - -/** - * Generate a URL to a case - */ -export function getCaseUrl( - request: KibanaRequest, - core: CoreStart, - spaces: SpacesPluginStart | undefined, - caseId: string, - owner: string -): string | null { - try { - const spaceId = getCurrentSpaceId({ request, spaces }); - const publicBaseUrl = core.http.basePath.publicBaseUrl; - - // getCaseViewPath returns a full URL when publicBaseUrl is provided - if (publicBaseUrl) { - return getCaseViewPath({ - publicBaseUrl, - spaceId, - caseId, - owner, - }); - } - - // Fallback: construct URL manually - const appRoute = getAppRoute(owner); - const path = `${appRoute}/cases/${caseId}`; - return buildFullUrl(request, core, spaceId, path); - } catch (error) { - return null; - } -} From 742b6d98133f7d14988224c1f1283ae7df897388 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Dec 2025 07:44:40 +0000 Subject: [PATCH 75/96] Changes from node scripts/lint_ts_projects --fix --- x-pack/platform/plugins/shared/onechat/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/onechat/tsconfig.json b/x-pack/platform/plugins/shared/onechat/tsconfig.json index e421d744528c7..f3020b6311fe5 100644 --- a/x-pack/platform/plugins/shared/onechat/tsconfig.json +++ b/x-pack/platform/plugins/shared/onechat/tsconfig.json @@ -95,6 +95,5 @@ "@kbn/core-notifications-browser", "@kbn/deeplinks-agent-builder", "@kbn/ui-theme", - "@kbn/cases-plugin" ] } From 485479088c1efbb6145507df6b9afa72cf835a79 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:06:53 +0000 Subject: [PATCH 76/96] Changes from node scripts/regenerate_moon_projects.js --update --- x-pack/platform/plugins/shared/onechat/moon.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/onechat/moon.yml b/x-pack/platform/plugins/shared/onechat/moon.yml index 945e80eaa6584..12e0ca3963591 100644 --- a/x-pack/platform/plugins/shared/onechat/moon.yml +++ b/x-pack/platform/plugins/shared/onechat/moon.yml @@ -99,7 +99,6 @@ dependsOn: - '@kbn/core-notifications-browser' - '@kbn/deeplinks-agent-builder' - '@kbn/ui-theme' - - '@kbn/cases-plugin' tags: - plugin - prod From b70225b7d74c4a5a37be525bce6b6e8cfcca187d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 10:15:35 +0100 Subject: [PATCH 77/96] roles --- .../project_roles/security/roles.yml | 12 ++++++++++++ .../project_roles/security/search_ai_lake/roles.yml | 2 ++ 2 files changed, 14 insertions(+) diff --git a/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/roles.yml b/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/roles.yml index a061241f705e9..7663f290bf002 100644 --- a/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/roles.yml +++ b/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/roles.yml @@ -63,6 +63,7 @@ viewer: - feature_visualize_v2.all - feature_savedQueryManagement.all - feature_dataQuality.all + - feature_agentBuilder.read resources: '*' run_as: [] @@ -159,6 +160,7 @@ editor: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all + - feature_agentBuilder.read resources: '*' run_as: [] @@ -218,6 +220,7 @@ t1_analyst: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all + - feature_agentBuilder.read resources: '*' t2_analyst: @@ -280,6 +283,7 @@ t2_analyst: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all + - feature_agentBuilder.read resources: '*' t3_analyst: @@ -362,6 +366,7 @@ t3_analyst: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all + - feature_agentBuilder.read resources: '*' threat_intelligence_analyst: @@ -432,6 +437,7 @@ threat_intelligence_analyst: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all + - feature_agentBuilder.read resources: '*' rule_author: @@ -513,6 +519,7 @@ rule_author: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all + - feature_agentBuilder.read resources: '*' soc_manager: @@ -610,6 +617,7 @@ soc_manager: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all + - feature_agentBuilder.read resources: '*' detections_admin: @@ -684,6 +692,7 @@ detections_admin: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all + - feature_agentBuilder.read resources: '*' platform_engineer: @@ -764,6 +773,7 @@ platform_engineer: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all + - feature_agentBuilder.read resources: '*' endpoint_operations_analyst: @@ -849,6 +859,7 @@ endpoint_operations_analyst: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all + - feature_agentBuilder.read resources: '*' endpoint_policy_manager: @@ -936,6 +947,7 @@ endpoint_policy_manager: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all + - feature_agentBuilder.read resources: '*' # admin role defined in elasticsearch controller diff --git a/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/search_ai_lake/roles.yml b/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/search_ai_lake/roles.yml index edeaf7e3eedc2..ff2892d4774c7 100644 --- a/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/search_ai_lake/roles.yml +++ b/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/search_ai_lake/roles.yml @@ -55,6 +55,7 @@ _search_ai_lake_analyst: - "feature_savedQueryManagement.read" - "feature_indexPatterns.read" - "feature_fleetv2.read" + - "feature_agentBuilder.read" resources: "*" _search_ai_lake_soc_manager: @@ -134,4 +135,5 @@ _search_ai_lake_soc_manager: - "feature_ml.all" - "feature_fleetv2.all" - "feature_advancedSettings.all" + - "feature_agentBuilder.read" resources: "*" From 6fb5cc011c2fa483b6557b23babf38c830bd1be7 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 10:56:57 +0100 Subject: [PATCH 78/96] rule attachment type --- .../attachments/attachment_types.ts | 12 -- .../server/attachment_types/index.ts | 2 - .../attachment_types/product_reference.ts | 64 ---------- .../security_solution/common/constants.ts | 2 +- .../new_agent_builder_attachment.tsx | 1 + .../public/agent_builder/helpers.test.tsx | 1 + .../rule_status_failed_callout.tsx | 4 +- .../components/ai_assistant/index.tsx | 4 +- .../rules_table/rules_table_toolbar.tsx | 4 +- .../entity_details/generic_right/footer.tsx | 31 +---- .../attachments/query_help.test.ts | 113 ------------------ .../agent_builder/attachments/query_help.ts | 80 ------------- .../attachments/register_attachments.ts | 4 +- .../server/agent_builder/attachments/rule.ts | 66 ++++++++++ .../server/agent_builder/jest.config.js | 1 + .../agent_builder/tools/helpers.test.ts | 1 + 16 files changed, 81 insertions(+), 309 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/rule.ts diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts index ae0850719905a..d5cdb3a411bf2 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts @@ -79,16 +79,4 @@ export interface ScreenContextAttachmentData { additional_data?: Record; } -export const productReferenceAttachmentDataSchema = z.object({ - text: z.string(), -}); - -/** - * Data for a product reference attachment. - */ -export interface ProductReferenceAttachmentData { - /** Text content describing the product reference or query */ - text: string; -} - export type AttachmentDataOf = AttachmentDataMap[Type]; diff --git a/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/index.ts b/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/index.ts index 9b99081c1d333..b2a046331bf38 100644 --- a/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/index.ts +++ b/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/index.ts @@ -10,7 +10,6 @@ import type { CoreSetup } from '@kbn/core-lifecycle-server'; import { createTextAttachmentType } from './text'; import { createEsqlAttachmentType } from './esql'; import { createScreenContextAttachmentType } from './screen_context'; -import { createProductReferenceAttachmentType } from './product_reference'; import type { AgentBuilderPlatformPluginStart, PluginSetupDependencies, @@ -30,7 +29,6 @@ export const registerAttachmentTypes = ({ createTextAttachmentType(), createScreenContextAttachmentType(), createEsqlAttachmentType(), - createProductReferenceAttachmentType(), ]; attachmentTypes.forEach((attachmentType) => { diff --git a/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts b/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts deleted file mode 100644 index 9159a177e06cf..0000000000000 --- a/x-pack/platform/plugins/shared/agent_builder_platform/server/attachment_types/product_reference.ts +++ /dev/null @@ -1,64 +0,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 Elastic License - * 2.0. - */ - -import type { ProductReferenceAttachmentData } from '@kbn/onechat-common/attachments'; -import { - AttachmentType, - productReferenceAttachmentDataSchema, -} from '@kbn/onechat-common/attachments'; -import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; -import { platformCoreTools } from '@kbn/onechat-common'; - -/** - * Creates the definition for the `product_reference` attachment type. - */ -export const createProductReferenceAttachmentType = (): AttachmentTypeDefinition< - AttachmentType.product_reference, - ProductReferenceAttachmentData -> => { - return { - id: AttachmentType.product_reference, - validate: (input) => { - const parseResult = productReferenceAttachmentDataSchema.safeParse(input); - if (parseResult.success) { - return { valid: true, data: parseResult.data }; - } else { - return { valid: false, error: parseResult.error.message }; - } - }, - format: (attachment) => { - return { - getRepresentation: () => { - return { type: 'text', value: formatProductReferenceData(attachment.data) }; - }, - }; - }, - getTools: () => [platformCoreTools.productDocumentation], - getAgentDescription: () => { - const description = `You have access to a product reference that needs to be queried for documentation. - -PRODUCT REFERENCE DATA: -{productReferenceData} - ---- - -1. Extract the query or topic from the product reference attachment. -2. Use the appropriate tools to provide a response`; - return description; - }, - }; -}; - -/** - * Formats product reference data for display. - * - * @param data - The product reference attachment data containing the text - * @returns Formatted string representation of the product reference data - */ -const formatProductReferenceData = (data: ProductReferenceAttachmentData): string => { - return data.text; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index c5f897078535e..137eab8c376a5 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -718,5 +718,5 @@ export const ESSENTIAL_ALERT_FIELDS: string[] = [ export enum SecurityAgentBuilderAttachments { alert = 'security.alert', entity = 'security.entity', - query_help = 'security.query_help', + rule = 'security.rule', } diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx index 024d6b6e5eb3c..f1c3668e22154 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx @@ -67,3 +67,4 @@ NewAgentBuilderAttachmentComponent.displayName = 'NewAgentBuilderAttachmentCompo * with attachment data. You may optionally override the default text. */ export const NewAgentBuilderAttachment = React.memo(NewAgentBuilderAttachmentComponent); + diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx index 0729f62ca1f84..38e67ed59c975 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx @@ -60,3 +60,4 @@ describe('filterAndStringifyAlertData', () => { expect(parsed).toEqual({}); }); }); + diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.tsx index 1cdc352558b32..4e175a9487077 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.tsx @@ -11,7 +11,6 @@ import { css } from '@emotion/react'; import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; import { NewChat } from '@kbn/elastic-assistant'; -import { AttachmentType } from '@kbn/onechat-common/attachments'; import { FormattedDate } from '../../../../common/components/formatted_date'; import type { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring'; import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring'; @@ -21,6 +20,7 @@ import { useAssistantAvailability } from '../../../../assistant/use_assistant_av import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { NewAgentBuilderAttachment } from '../../../../agent_builder/components/new_agent_builder_attachment'; import { useAgentBuilderAttachment } from '../../../../agent_builder/hooks/use_agent_builder_attachment'; +import { SecurityAgentBuilderAttachments } from '../../../../../common/constants'; interface RuleStatusFailedCallOutProps { ruleNameForChat: string; @@ -64,7 +64,7 @@ const RuleStatusFailedCallOutComponent: React.FC = [message, ruleName, dataSources] ); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: AttachmentType.product_reference, + attachmentType: SecurityAgentBuilderAttachments.rule, attachmentData, attachmentPrompt: i18n.ASK_ASSISTANT_USER_PROMPT, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx index 7f90c753a3381..310bf37eeb887 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/ai_assistant/index.tsx @@ -104,11 +104,11 @@ Proposed solution should be valid and must not contain new line symbols (\\n)`; const attachmentData = useMemo(() => { const queryField = getFields().queryBar; const { query } = (queryField.value as DefineStepRule['queryBar']).query; - return { query: query ?? '', queryLanguage: language }; + return { text: JSON.stringify({ query: query ?? '', queryLanguage: language }) }; }, [getFields, language]); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: SecurityAgentBuilderAttachments.query_help, + attachmentType: SecurityAgentBuilderAttachments.rule, attachmentData, attachmentPrompt: i18n.ASK_ASSISTANT_USER_PROMPT(languageName), }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx index a8908c2dd426a..1330b13a652e6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx @@ -8,7 +8,6 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { NewChat } from '@kbn/elastic-assistant'; -import { AttachmentType } from '@kbn/onechat-common/attachments'; import { useUserData } from '../../../../detections/components/user_info'; import { TabNavigation } from '../../../../common/components/navigation/tab_navigation'; import { usePrebuiltRulesStatus } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_status'; @@ -21,6 +20,7 @@ import * as i18nAssistant from '../../../common/translations'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { NewAgentBuilderAttachment } from '../../../../agent_builder/components/new_agent_builder_attachment'; import { useAgentBuilderAttachment } from '../../../../agent_builder/hooks/use_agent_builder_attachment'; +import { SecurityAgentBuilderAttachments } from '../../../../../common/constants'; export enum AllRulesTabs { management = 'management', @@ -107,7 +107,7 @@ export const RulesTableToolbar = React.memo(() => { [selectedRules] ); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: AttachmentType.product_reference, + attachmentType: SecurityAgentBuilderAttachments.rule, attachmentData, attachmentPrompt: i18nAssistant.EXPLAIN_THEN_SUMMARIZE_RULE_DETAILS, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx index 26a8fcadefd39..d47aede92c9b6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/generic_right/footer.tsx @@ -11,7 +11,6 @@ import type { EntityEcs } from '@kbn/securitysolution-ecs/src/entity'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { i18n } from '@kbn/i18n'; import { NewChatByTitle } from '@kbn/elastic-assistant'; -import { AttachmentType } from '@kbn/onechat-common/attachments'; import { DocumentEventTypes } from '../../../common/lib/telemetry'; import { TakeAction } from '../shared/components/take_action'; import { @@ -25,9 +24,6 @@ import { ASK_AI_ASSISTANT } from '../shared/translations'; import { useAssetInventoryAssistant } from './hooks/use_asset_inventory_assistant'; import type { AssetCriticalityLevel } from '../../../../common/api/entity_analytics/asset_criticality'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; -import { NewAgentBuilderAttachment } from '../../../agent_builder/components/new_agent_builder_attachment'; -import { useAgentBuilderAttachment } from '../../../agent_builder/hooks/use_agent_builder_attachment'; -import { ENTITY_ANALYSIS } from '../../../agent_builder/components/prompts'; interface GenericEntityFlyoutFooterProps { entityId: EntityEcs['id']; @@ -56,22 +52,6 @@ export const GenericEntityFlyoutFooter = ({ const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); - const attachmentData = useMemo(() => { - return { - text: { - ...entityFields, - 'asset.criticality': assetCriticalityLevel ? [assetCriticalityLevel] : undefined, - }, - }; - }, [entityFields, assetCriticalityLevel]); - - // TODO confirm behavior with @maxcold https://github.com/elastic/kibana/pull/234324 - const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ - attachmentType: AttachmentType.product_reference, - attachmentData, - attachmentPrompt: ENTITY_ANALYSIS, - }); - const openDocumentFlyout = useCallback(() => { openFlyout({ right: { @@ -110,16 +90,9 @@ export const GenericEntityFlyoutFooter = ({ {isPreviewMode && {fullDetailsLink}} - {showAssistant && ( + {showAssistant && !isAgentBuilderEnabled && ( - {isAgentBuilderEnabled ? ( - - ) : ( - - )} + )} diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts deleted file mode 100644 index 475c0db225a5f..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.test.ts +++ /dev/null @@ -1,113 +0,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 Elastic License - * 2.0. - */ - -import type { Attachment } from '@kbn/onechat-common/attachments'; -import { platformCoreTools } from '@kbn/onechat-common'; -import { SecurityAgentBuilderAttachments } from '../../../common/constants'; -import { createQueryHelpAttachmentType } from './query_help'; - -describe('createQueryHelpAttachmentType', () => { - const attachmentType = createQueryHelpAttachmentType(); - - describe('validate', () => { - it('returns valid when query help data is valid', async () => { - const input = { query: 'SELECT * FROM test', queryLanguage: 'esql' }; - - const result = await attachmentType.validate(input); - - expect(result.valid).toBe(true); - if (result.valid) { - expect(result.data).toEqual(input); - } - }); - - it('returns invalid when query field is missing', async () => { - const input = { queryLanguage: 'esql' }; - - const result = await attachmentType.validate(input); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toBeDefined(); - } - }); - - it('returns invalid when queryLanguage field is missing', async () => { - const input = { query: 'SELECT * FROM test' }; - - const result = await attachmentType.validate(input); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toBeDefined(); - } - }); - - it('returns invalid when query is not a string', async () => { - const input = { query: 123, queryLanguage: 'esql' }; - - const result = await attachmentType.validate(input); - - expect(result.valid).toBe(false); - if (!result.valid) { - expect(result.error).toBeDefined(); - } - }); - }); - - describe('format', () => { - it('includes query and queryLanguage in formatted output', async () => { - const attachment: Attachment = { - id: 'test-id', - type: SecurityAgentBuilderAttachments.query_help, - data: { query: 'SELECT * FROM test', queryLanguage: 'esql' }, - }; - - const formatted = await attachmentType.format(attachment); - const representation = await formatted.getRepresentation(); - - expect(representation.type).toBe('text'); - expect(representation.value).toContain('Query: SELECT * FROM test'); - expect(representation.value).toContain('Query Language: esql'); - }); - - it('throws error when attachment data is invalid', () => { - const attachment: Attachment = { - id: 'test-id', - type: SecurityAgentBuilderAttachments.query_help, - data: { invalid: 'data' }, - }; - - expect(() => attachmentType.format(attachment)).toThrow( - 'Invalid query help attachment data for attachment test-id' - ); - }); - }); - - describe('getTools', () => { - it('returns expected tools', () => { - const tools = attachmentType.getTools?.(); - - expect(tools).toBeDefined(); - if (tools) { - expect(tools).toContain(platformCoreTools.generateEsql); - expect(tools).toContain(platformCoreTools.productDocumentation); - } - }); - }); - - describe('getAgentDescription', () => { - it('returns expected description', () => { - const description = attachmentType.getAgentDescription?.(); - - expect(description).toContain('broken query'); - expect(description).toContain('QUERY HELP DATA'); - expect(description).toContain('queryLanguage'); - expect(description).toContain('generateEsql'); - }); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts deleted file mode 100644 index 1432671bf9b8f..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/query_help.ts +++ /dev/null @@ -1,80 +0,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 Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod'; -import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; -import type { Attachment } from '@kbn/onechat-common/attachments'; -import { platformCoreTools } from '@kbn/onechat-common'; -import { SecurityAgentBuilderAttachments } from '../../../common/constants'; - -export const queryHelpAttachmentDataSchema = z.object({ - query: z.string(), - queryLanguage: z.string(), -}); - -export type QueryHelpAttachmentData = z.infer; - -const isQueryHelpAttachmentData = (data: unknown): data is QueryHelpAttachmentData => { - return queryHelpAttachmentDataSchema.safeParse(data).success; -}; - -export const createQueryHelpAttachmentType = (): AttachmentTypeDefinition => { - return { - id: SecurityAgentBuilderAttachments.query_help, - validate: (input) => { - const parseResult = queryHelpAttachmentDataSchema.safeParse(input); - if (parseResult.success) { - return { valid: true, data: parseResult.data }; - } else { - return { valid: false, error: parseResult.error.message }; - } - }, - format: (attachment: Attachment) => { - // Extract data to allow proper type narrowing - const data = attachment.data; - // Necessary because we cannot currently use the AttachmentType type as agent is not - // registered with enum AttachmentType in onechat attachment_types.ts - if (!isQueryHelpAttachmentData(data)) { - throw new Error(`Invalid query help attachment data for attachment ${attachment.id}`); - } - return { - getRepresentation: () => { - return { type: 'text', value: formatQueryHelpData(data) }; - }, - }; - }, - getTools: () => { - const tools: string[] = [ - platformCoreTools.generateEsql, - platformCoreTools.productDocumentation, - ]; - return tools; - }, - getAgentDescription: () => { - const description = `The following is a broken query: {query}. Generate a new working query using the generateEsql tool (only if queryLanguage is 'esql') and productDocumentationTool when appropriate. - -QUERY HELP DATA: -{queryHelpData} - ---- - -1. Check the queryLanguage from the query attachment provided. -2. Use the appropriate tools to provide a corrected query.`; - return description; - }, - }; -}; - -/** - * Formats query help data for display. - * - * @param data - The query help attachment data containing the query and queryLanguage - * @returns Formatted string representation of the query help data - */ -const formatQueryHelpData = (data: QueryHelpAttachmentData): string => { - return `Query: ${data.query}\nQuery Language: ${data.queryLanguage}`; -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts index 4bac1a57eb143..ef3839bada02d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/register_attachments.ts @@ -6,9 +6,9 @@ */ import type { OnechatPluginSetup } from '@kbn/onechat-plugin/server'; +import { createRuleAttachmentType } from './rule'; import { createAlertAttachmentType } from './alert'; import { createEntityAttachmentType } from './entity'; -import { createQueryHelpAttachmentType } from './query_help'; /** * Registers all security agent builder attachments with the onechat plugin @@ -16,5 +16,5 @@ import { createQueryHelpAttachmentType } from './query_help'; export const registerAttachments = async (onechat: OnechatPluginSetup) => { onechat.attachments.registerType(createAlertAttachmentType()); onechat.attachments.registerType(createEntityAttachmentType()); - onechat.attachments.registerType(createQueryHelpAttachmentType()); + onechat.attachments.registerType(createRuleAttachmentType()); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/rule.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/rule.ts new file mode 100644 index 0000000000000..f46fc013dcd19 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/rule.ts @@ -0,0 +1,66 @@ +/* + * 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. + */ + +import type { AttachmentTypeDefinition } from '@kbn/onechat-server/attachments'; +import type { Attachment } from '@kbn/onechat-common/attachments'; +import { platformCoreTools } from '@kbn/onechat-common'; +import { z } from '@kbn/zod'; +import { SecurityAgentBuilderAttachments } from '../../../common/constants'; + +export const ruleAttachmentDataSchema = z.object({ + text: z.string(), +}); + +type RuleAttachmentData = z.infer; + +/** + * Type guard to narrow attachment data to AlertAttachmentData + */ +const isRuleAttachmentData = (data: unknown): data is RuleAttachmentData => { + return ruleAttachmentDataSchema.safeParse(data).success; +}; +export const createRuleAttachmentType = (): AttachmentTypeDefinition => { + return { + id: SecurityAgentBuilderAttachments.rule, + validate: (input) => { + const parseResult = ruleAttachmentDataSchema.safeParse(input); + if (parseResult.success) { + return { valid: true, data: parseResult.data }; + } else { + return { valid: false, error: parseResult.error.message }; + } + }, + format: (attachment: Attachment) => { + // Extract data to allow proper type narrowing + const data = attachment.data; + // Necessary because we cannot currently use the AttachmentType type as agent is not + // registered with enum AttachmentType in onechat attachment_types.ts + if (!isRuleAttachmentData(data)) { + throw new Error(`Invalid rule attachment data for attachment ${attachment.id}`); + } + return { + getRepresentation: () => { + return { type: 'text', value: formatProductReferenceData(attachment.data) }; + }, + }; + }, + getTools: () => [platformCoreTools.generateEsql, platformCoreTools.productDocumentation], + getAgentDescription: () => { + const description = `You have access to a rule or query. + +{ruleData} + +1. Extract the query or topic from the rule attachment. +2. Use the appropriate tools to provide a response`; + return description; + }, + }; +}; + +const formatProductReferenceData = (data: RuleAttachmentData): string => { + return data.text; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js index f8b206cc154a0..a6a38d771b361 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js @@ -17,3 +17,4 @@ module.exports = { ], moduleNameMapper: require('../__mocks__/module_name_map'), }; + diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts index 17d784638daba..d18529d9d3737 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts @@ -40,3 +40,4 @@ describe('getSpaceIdFromRequest', () => { expect(spaceId).toBe(DEFAULT_SPACE_ID); }); }); + From 22fb29a630608a8c0aa801023146b78132f622f3 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 11:01:46 +0100 Subject: [PATCH 79/96] type fix --- .../server/agent_builder/attachments/rule.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/rule.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/rule.ts index f46fc013dcd19..92e683f4c608b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/rule.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/rule.ts @@ -44,7 +44,7 @@ export const createRuleAttachmentType = (): AttachmentTypeDefinition => { } return { getRepresentation: () => { - return { type: 'text', value: formatProductReferenceData(attachment.data) }; + return { type: 'text', value: formatRuleData(data) }; }, }; }, @@ -61,6 +61,6 @@ export const createRuleAttachmentType = (): AttachmentTypeDefinition => { }; }; -const formatProductReferenceData = (data: RuleAttachmentData): string => { +const formatRuleData = (data: RuleAttachmentData): string => { return data.text; }; From 73db09d6b3524cea86a67f652893010d95aa6b44 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 11:13:24 +0100 Subject: [PATCH 80/96] test fix --- .../agent_builder/hooks/use_agent_builder_attachment.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx index a865c7f0d9826..e83c6ed6266aa 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/hooks/use_agent_builder_attachment.test.tsx @@ -86,7 +86,7 @@ describe('useAgentBuilderAttachment', () => { { id: 'alert-1234567890', type: 'alert', - alert: 'test alert data', + data: { alert: 'test alert data' }, }, ], sessionTag: 'security', From 46d3e3b55047267e70bc026ab8a94e5e98b7e501 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 11:14:00 +0100 Subject: [PATCH 81/96] type fixing --- .../onechat/onechat-common/attachments/attachment_types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts index d5cdb3a411bf2..18e32ca2d028d 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachment_types.ts @@ -16,14 +16,12 @@ export enum AttachmentType { screenContext = 'screen_context', text = 'text', esql = 'esql', - product_reference = 'product_reference', } interface AttachmentDataMap { [AttachmentType.esql]: EsqlAttachmentData; [AttachmentType.text]: TextAttachmentData; [AttachmentType.screenContext]: ScreenContextAttachmentData; - [AttachmentType.product_reference]: ProductReferenceAttachmentData; } export const esqlAttachmentDataSchema = z.object({ From 99d4ca3492c6f9fae33a0213afdb5219a765f683 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 11:14:18 +0100 Subject: [PATCH 82/96] moar --- .../packages/shared/onechat/onechat-common/attachments/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts index d50e9a4abfa2b..e9a0f6d8897a1 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts @@ -18,9 +18,7 @@ export { textAttachmentDataSchema, esqlAttachmentDataSchema, screenContextAttachmentDataSchema, - productReferenceAttachmentDataSchema, type TextAttachmentData, type ScreenContextAttachmentData, type EsqlAttachmentData, - type ProductReferenceAttachmentData, } from './attachment_types'; From 2efe7e32b52f29d7794f0b29b20fc4f567cfa386 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 11:16:17 +0100 Subject: [PATCH 83/96] another --- .../shared/onechat/onechat-common/attachments/attachments.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts index c1f4c2e62bdb2..a63cd31b4b76e 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/attachments.ts @@ -29,7 +29,6 @@ export interface Attachment< export type TextAttachment = Attachment; export type ScreenContextAttachment = Attachment; export type EsqlAttachment = Attachment; -export type ProductReferenceAttachment = Attachment; /** * Input version of an attachment, where the id is optional From 5359728a0a2c7c556afe969bb0f80369e40a74c6 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 11:16:59 +0100 Subject: [PATCH 84/96] nother --- .../packages/shared/onechat/onechat-common/attachments/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts index e9a0f6d8897a1..efe8c0d98169a 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/attachments/index.ts @@ -11,7 +11,6 @@ export type { TextAttachment, ScreenContextAttachment, EsqlAttachment, - ProductReferenceAttachment, } from './attachments'; export { AttachmentType, From 1751dbeec284caaa4402810eae8c90fb9d96e15b Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 11:22:07 +0100 Subject: [PATCH 85/96] rm export file --- .../server/agent_builder/attachments/index.ts | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts deleted file mode 100644 index a48b74eeef58d..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/attachments/index.ts +++ /dev/null @@ -1,10 +0,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 Elastic License - * 2.0. - */ - -export { createAlertAttachmentType } from './alert'; -export { createEntityAttachmentType } from './entity'; -export { createQueryHelpAttachmentType } from './query_help'; From 8f183ee9f6eeb5f4814bde08a06c384eb4e4fddd Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 11:30:22 +0100 Subject: [PATCH 86/96] rm unused prompt --- .../public/agent_builder/components/prompts.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts index 021c3a6ff978e..7d3a7e4a4be5e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/prompts.ts @@ -77,17 +77,3 @@ Formatting Requirements: - Organize the response into visually distinct sections. - Use concise, actionable language. - Include relevant emojis in section headers for visual clarity (e.g., 📝, 🛡️, 🔍, 📚).`; - -export const ENTITY_ANALYSIS = `Analyze asset data described above to provide security insights. The data contains the context of a specific asset (e.g., a host, user, service or cloud resource). Your response must be structured, contextual, and provide a general analysis based on the structure below. -Your response must be in markdown format and include the following sections: -**1. 🔍 Asset Overview** - - Begin by acknowledging the asset you are analyzing using its primary identifiers (e.g., "Analyzing host \`[host.name]\` with IP \`[host.ip]\`"). - - Provide a concise summary of the asset's most critical attributes from the provided context. - - Describe its key relationships and dependencies (e.g., "This asset is part of the \`[cloud.project.name]\` project and is located in the \`[cloud.availability_zone]\` zone."). -**2. 💡 Investigation & Analytics** - - Based on the asset's type and attributes, suggest potential investigation paths or common attack vectors. - - **Generate one contextual ES|QL query** to help the user investigate further. Your generated query should address a common analytical question related to the asset type and sub type. Suggest other possible queries and ask if the user wants to generate more queries. -**General Instructions:** -- **Context Awareness:** Your entire analysis must be derived from the provided asset context. If a piece of information is not available in the context state that and proceed with the available data. -- **Query Generation:** When generating a query, your primary output for that section should be a valid, ready-to-use ES|QL query based on the asset's schema. Use ES|QL tool for query generation. Format all queries as code blocks. -- **Formatting:** Use markdown headers, tables, code blocks, and bullet points to ensure the output is clear, organized, and easily readable. Use concise, actionable language.`; From 5e29ae069ef9f7835e60189ce587bf54cfa8f556 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 11:44:54 +0100 Subject: [PATCH 87/96] attack discovery availability --- .../attack_discovery_search_tool.test.ts | 4 +- .../tools/attack_discovery_search_tool.ts | 38 +++++++++++++++++-- .../agent_builder/tools/register_tools.ts | 2 +- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts index 66e08a58d0921..c9edbf740701d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.test.ts @@ -15,8 +15,8 @@ jest.mock('@kbn/onechat-genai-utils', () => ({ })); describe('attackDiscoverySearchTool', () => { - const { mockLogger, mockEsClient, mockRequest } = createToolTestMocks(); - const tool = attackDiscoverySearchTool(); + const { mockCore, mockLogger, mockEsClient, mockRequest } = createToolTestMocks(); + const tool = attackDiscoverySearchTool(mockCore); beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts index b4547e0d22119..e0cfd1f5dfc11 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/attack_discovery_search_tool.ts @@ -7,8 +7,9 @@ import { z } from '@kbn/zod'; import { ToolType, ToolResultType } from '@kbn/onechat-common'; -import type { BuiltinToolDefinition } from '@kbn/onechat-server'; +import type { BuiltinToolDefinition, ToolAvailabilityContext } from '@kbn/onechat-server'; import { executeEsql } from '@kbn/onechat-genai-utils'; +import type { CoreSetup } from '@kbn/core-lifecycle-server'; import { getSpaceIdFromRequest } from './helpers'; import { securityTool } from './constants'; @@ -22,14 +23,43 @@ const attackDiscoverySearchSchema = z.object({ export const SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID = securityTool('attack_discovery_search'); -export const attackDiscoverySearchTool = (): BuiltinToolDefinition< - typeof attackDiscoverySearchSchema -> => { +export const attackDiscoverySearchTool = ( + core: CoreSetup +): BuiltinToolDefinition => { return { id: SECURITY_ATTACK_DISCOVERY_SEARCH_TOOL_ID, type: ToolType.builtin, description: `Search and analyze attack discoveries. Use this tool to find attack discoveries related to specific alerts by providing alert IDs. The tool searches the kibana.alert.attack_discovery.alert_ids field. Automatically queries both scheduled and ad-hoc attack discovery indices for the current space. Limits results to 5 attack discoveries.`, schema: attackDiscoverySearchSchema, + availability: { + cacheMode: 'space', + handler: async ({ spaceId }: ToolAvailabilityContext) => { + try { + const [coreStart] = await core.getStartServices(); + const esClient = coreStart.elasticsearch.client.asInternalUser; + const index = `.alerts-security.attack.discovery.alerts-${spaceId}*,.adhoc.alerts-security.attack.discovery.alerts-${spaceId}`; + + const indexExists = await esClient.indices.exists({ + index, + }); + if (indexExists) { + return { status: 'available' }; + } + + return { + status: 'unavailable', + reason: 'Attack discovery index does not exist for this space', + }; + } catch (error) { + return { + status: 'unavailable', + reason: `Failed to check attack discovery index availability: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + }; + } + }, + }, handler: async ({ alertIds }, { request, esClient, logger }) => { const spaceId = getSpaceIdFromRequest(request); diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts index b0c03fb7675ca..08f1d138f0c04 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/register_tools.ts @@ -15,6 +15,6 @@ import { entityRiskScoreTool } from './entity_risk_score_tool'; */ export const registerTools = async (onechat: OnechatPluginSetup, core: CoreSetup) => { onechat.tools.register(entityRiskScoreTool(core)); - onechat.tools.register(attackDiscoverySearchTool()); + onechat.tools.register(attackDiscoverySearchTool(core)); onechat.tools.register(securityLabsSearchTool(core)); }; From 33c6c195bbf51c9cd7ea7f9096b062df7c1cf007 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:13:07 +0000 Subject: [PATCH 88/96] Changes from node scripts/eslint_all_files --no-cache --fix --- .../agent_builder/components/new_agent_builder_attachment.tsx | 1 - .../security_solution/public/agent_builder/helpers.test.tsx | 1 - .../security_solution/server/agent_builder/jest.config.js | 1 - .../security_solution/server/agent_builder/tools/helpers.test.ts | 1 - 4 files changed, 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx index f1c3668e22154..024d6b6e5eb3c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx @@ -67,4 +67,3 @@ NewAgentBuilderAttachmentComponent.displayName = 'NewAgentBuilderAttachmentCompo * with attachment data. You may optionally override the default text. */ export const NewAgentBuilderAttachment = React.memo(NewAgentBuilderAttachmentComponent); - diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx index 38e67ed59c975..0729f62ca1f84 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx @@ -60,4 +60,3 @@ describe('filterAndStringifyAlertData', () => { expect(parsed).toEqual({}); }); }); - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js index a6a38d771b361..f8b206cc154a0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/jest.config.js @@ -17,4 +17,3 @@ module.exports = { ], moduleNameMapper: require('../__mocks__/module_name_map'), }; - diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts index d18529d9d3737..17d784638daba 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/helpers.test.ts @@ -40,4 +40,3 @@ describe('getSpaceIdFromRequest', () => { expect(spaceId).toBe(DEFAULT_SPACE_ID); }); }); - From 56079e4630f41caf57bcb093fafbe35f78e62de4 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 12:57:25 +0100 Subject: [PATCH 89/96] role fix --- .../project_roles/security/roles.yml | 22 +++++++++---------- .../security/search_ai_lake/roles.yml | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/roles.yml b/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/roles.yml index 7663f290bf002..55af5ec6d8a1c 100644 --- a/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/roles.yml +++ b/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/roles.yml @@ -160,7 +160,7 @@ editor: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all - - feature_agentBuilder.read + - feature_agentBuilder.all resources: '*' run_as: [] @@ -220,7 +220,7 @@ t1_analyst: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all - - feature_agentBuilder.read + - feature_agentBuilder.all resources: '*' t2_analyst: @@ -283,7 +283,7 @@ t2_analyst: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all - - feature_agentBuilder.read + - feature_agentBuilder.all resources: '*' t3_analyst: @@ -366,7 +366,7 @@ t3_analyst: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all - - feature_agentBuilder.read + - feature_agentBuilder.all resources: '*' threat_intelligence_analyst: @@ -437,7 +437,7 @@ threat_intelligence_analyst: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all - - feature_agentBuilder.read + - feature_agentBuilder.all resources: '*' rule_author: @@ -519,7 +519,7 @@ rule_author: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all - - feature_agentBuilder.read + - feature_agentBuilder.all resources: '*' soc_manager: @@ -617,7 +617,7 @@ soc_manager: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all - - feature_agentBuilder.read + - feature_agentBuilder.all resources: '*' detections_admin: @@ -692,7 +692,7 @@ detections_admin: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all - - feature_agentBuilder.read + - feature_agentBuilder.all resources: '*' platform_engineer: @@ -773,7 +773,7 @@ platform_engineer: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all - - feature_agentBuilder.read + - feature_agentBuilder.all resources: '*' endpoint_operations_analyst: @@ -859,7 +859,7 @@ endpoint_operations_analyst: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all - - feature_agentBuilder.read + - feature_agentBuilder.all resources: '*' endpoint_policy_manager: @@ -947,7 +947,7 @@ endpoint_policy_manager: - feature_maps_v2.all - feature_visualize_v2.all - feature_savedQueryManagement.all - - feature_agentBuilder.read + - feature_agentBuilder.all resources: '*' # admin role defined in elasticsearch controller diff --git a/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/search_ai_lake/roles.yml b/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/search_ai_lake/roles.yml index ff2892d4774c7..0d0785c6b34ba 100644 --- a/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/search_ai_lake/roles.yml +++ b/src/platform/packages/shared/kbn-es/src/serverless_resources/project_roles/security/search_ai_lake/roles.yml @@ -55,7 +55,7 @@ _search_ai_lake_analyst: - "feature_savedQueryManagement.read" - "feature_indexPatterns.read" - "feature_fleetv2.read" - - "feature_agentBuilder.read" + - "feature_agentBuilder.all" resources: "*" _search_ai_lake_soc_manager: @@ -135,5 +135,5 @@ _search_ai_lake_soc_manager: - "feature_ml.all" - "feature_fleetv2.all" - "feature_advancedSettings.all" - - "feature_agentBuilder.read" + - "feature_agentBuilder.all" resources: "*" From 4bc5f23c5620e8472ad31d834dbfaed3f9306ef8 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 13:27:35 +0100 Subject: [PATCH 90/96] rm todo --- .../plugins/security_solution/public/flyout/ease/footer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx index a7a5ba4120955..edbc98b13a5d8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx @@ -40,14 +40,14 @@ export const PanelFooter = memo(() => { dataFormattedForFieldBrowser, isAlert, }); - // TODO enable agent builder for EASE roles + const isAgentBuilderEnabled = useIsExperimentalFeatureEnabled('agentBuilderEnabled'); const alertData = useMemo(() => { const rawData = getRawData(dataFormattedForFieldBrowser ?? []); return filterAndStringifyAlertData(rawData); }, [dataFormattedForFieldBrowser]); - // This will not work until we add permissions to EASE roles for read_onechat + const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ attachmentType: SecurityAgentBuilderAttachments.alert, attachmentData: { alert: alertData }, From 03a87298e643be5b9c64465c3bcd1aff48021f1d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 13:59:19 +0100 Subject: [PATCH 91/96] maxim PR comments --- .../new_agent_builder_attachment.tsx | 20 ++++++++----------- .../public/agent_builder/helpers.test.tsx | 12 +++++------ .../public/agent_builder/helpers.tsx | 13 ++++-------- .../flyout/document_details/right/footer.tsx | 4 ++-- .../public/flyout/ease/footer.tsx | 4 ++-- 5 files changed, 22 insertions(+), 31 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx index 024d6b6e5eb3c..f7863e96216ee 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx @@ -7,7 +7,7 @@ import type { EuiButtonColor, IconType } from '@elastic/eui'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import React from 'react'; +import React, { memo } from 'react'; import type { EuiButtonEmptySizes } from '@elastic/eui/src/components/button/button_empty/button_empty'; import * as i18n from './translations'; @@ -35,13 +35,17 @@ export interface NewAgentBuilderAttachmentProps { text?: string; } -const NewAgentBuilderAttachmentComponent: React.FC = ({ +/** + * `NewAgentBuilderAttachment` displays a button that opens the agent builder flyout + * with attachment data. You may optionally override the default text. + */ +export const NewAgentBuilderAttachment = memo(function NewAgentBuilderAttachment({ color = 'primary', iconType = 'machineLearningApp', onClick, size = 'm', text = i18n.VIEW_IN_AGENT_BUILDER, -}) => { +}: NewAgentBuilderAttachmentProps) { return ( ); -}; - -NewAgentBuilderAttachmentComponent.displayName = 'NewAgentBuilderAttachmentComponent'; - -/** - * `NewAgentBuilderAttachment` displays a button that opens the agent builder flyout - * with attachment data. You may optionally override the default text. - */ -export const NewAgentBuilderAttachment = React.memo(NewAgentBuilderAttachmentComponent); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx index 0729f62ca1f84..5189d56149489 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.test.tsx @@ -6,9 +6,9 @@ */ import { ESSENTIAL_ALERT_FIELDS } from '../../common'; -import { filterAndStringifyAlertData } from './helpers'; +import { stringifyEssentialAlertData } from './helpers'; -describe('filterAndStringifyAlertData', () => { +describe('stringifyEssentialAlertData', () => { it('filters to essential fields only', () => { const rawData: Record = { [ESSENTIAL_ALERT_FIELDS[0]]: ['value1'], @@ -17,7 +17,7 @@ describe('filterAndStringifyAlertData', () => { anotherNonEssential: ['shouldAlsoBeExcluded'], }; - const result = filterAndStringifyAlertData(rawData); + const result = stringifyEssentialAlertData(rawData); const parsed = JSON.parse(result); expect(parsed).toHaveProperty(ESSENTIAL_ALERT_FIELDS[0]); @@ -32,7 +32,7 @@ describe('filterAndStringifyAlertData', () => { field2: ['value2'], }; - const result = filterAndStringifyAlertData(rawData); + const result = stringifyEssentialAlertData(rawData); const parsed = JSON.parse(result); expect(Object.keys(parsed).length).toBe(0); @@ -43,7 +43,7 @@ describe('filterAndStringifyAlertData', () => { [ESSENTIAL_ALERT_FIELDS[0]]: ['value1'], }; - const result = filterAndStringifyAlertData(rawData); + const result = stringifyEssentialAlertData(rawData); expect(() => JSON.parse(result)).not.toThrow(); expect(JSON.parse(result)).toEqual({ @@ -54,7 +54,7 @@ describe('filterAndStringifyAlertData', () => { it('handles empty input', () => { const rawData: Record = {}; - const result = filterAndStringifyAlertData(rawData); + const result = stringifyEssentialAlertData(rawData); const parsed = JSON.parse(result); expect(parsed).toEqual({}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.tsx index 432c3ac6fb81b..c5aeb306d502e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/helpers.tsx @@ -5,19 +5,14 @@ * 2.0. */ +import { pick } from 'lodash'; + import { ESSENTIAL_ALERT_FIELDS } from '../../common'; /** * Filters raw alert data to only include essential fields and stringifies the result. * This reduces context window usage by keeping only the most relevant information. */ -export const filterAndStringifyAlertData = (rawData: Record): string => { - const filteredData = ESSENTIAL_ALERT_FIELDS.reduce((acc, key) => { - if (key in rawData) { - acc[key] = rawData[key]; - } - return acc; - }, {} as Record); - - return JSON.stringify(filteredData); +export const stringifyEssentialAlertData = (rawData: Record): string => { + return JSON.stringify(pick(rawData, ESSENTIAL_ALERT_FIELDS)); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx index 2af0b79ecab1f..115c03bc11bbf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/footer.tsx @@ -23,7 +23,7 @@ import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_exper import { NewAgentBuilderAttachment } from '../../../agent_builder/components/new_agent_builder_attachment'; import { useAgentBuilderAttachment } from '../../../agent_builder/hooks/use_agent_builder_attachment'; import { getRawData } from '../../../assistant/helpers'; -import { filterAndStringifyAlertData } from '../../../agent_builder/helpers'; +import { stringifyEssentialAlertData } from '../../../agent_builder/helpers'; import { SecurityAgentBuilderAttachments } from '../../../../common/constants'; export const ASK_AI_ASSISTANT = i18n.translate( @@ -54,7 +54,7 @@ export const PanelFooter: FC = ({ isRulePreview }) => { const alertData = useMemo(() => { const rawData = getRawData(dataFormattedForFieldBrowser ?? []); - return filterAndStringifyAlertData(rawData); + return stringifyEssentialAlertData(rawData); }, [dataFormattedForFieldBrowser]); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx index edbc98b13a5d8..174350126ca78 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ease/footer.tsx @@ -17,7 +17,7 @@ import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experime import { NewAgentBuilderAttachment } from '../../agent_builder/components/new_agent_builder_attachment'; import { useAgentBuilderAttachment } from '../../agent_builder/hooks/use_agent_builder_attachment'; import { getRawData } from '../../assistant/helpers'; -import { filterAndStringifyAlertData } from '../../agent_builder/helpers'; +import { stringifyEssentialAlertData } from '../../agent_builder/helpers'; import { SecurityAgentBuilderAttachments } from '../../../common/constants'; import { ALERT_ATTACHMENT_PROMPT } from '../../agent_builder/components/prompts'; @@ -45,7 +45,7 @@ export const PanelFooter = memo(() => { const alertData = useMemo(() => { const rawData = getRawData(dataFormattedForFieldBrowser ?? []); - return filterAndStringifyAlertData(rawData); + return stringifyEssentialAlertData(rawData); }, [dataFormattedForFieldBrowser]); const { openAgentBuilderFlyout } = useAgentBuilderAttachment({ From 63e3b9e34f181a4503591c933bd6fe1be7f6cab1 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 14:21:25 +0100 Subject: [PATCH 92/96] robot button! --- .../public/components/nav_control/onechat_nav_control.tsx | 4 ++-- x-pack/platform/plugins/shared/onechat/public/index.ts | 2 ++ .../solutions/security/plugins/security_solution/kibana.jsonc | 3 ++- .../agent_builder/components/new_agent_builder_attachment.tsx | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/x-pack/platform/plugins/shared/onechat/public/components/nav_control/onechat_nav_control.tsx b/x-pack/platform/plugins/shared/onechat/public/components/nav_control/onechat_nav_control.tsx index aea3651bbc041..712165a7cd0ab 100644 --- a/x-pack/platform/plugins/shared/onechat/public/components/nav_control/onechat_nav_control.tsx +++ b/x-pack/platform/plugins/shared/onechat/public/components/nav_control/onechat_nav_control.tsx @@ -18,7 +18,7 @@ interface OnechatNavControlServices { onechat: OnechatPluginStart; } -const iconType: React.FC> = (props) => ( +export const onechatIconType: React.FC> = (props) => ( @@ -32,7 +32,7 @@ const iconType: React.FC> = (props) => ( ); const RobotIcon = ({ size = 'm', ...rest }: Omit) => { - return ; + return ; }; const buttonCss = css` diff --git a/x-pack/platform/plugins/shared/onechat/public/index.ts b/x-pack/platform/plugins/shared/onechat/public/index.ts index 9833cd7847c21..5b3aded367b7e 100644 --- a/x-pack/platform/plugins/shared/onechat/public/index.ts +++ b/x-pack/platform/plugins/shared/onechat/public/index.ts @@ -14,8 +14,10 @@ import type { ConfigSchema, } from './types'; import { OnechatPlugin } from './plugin'; +import { onechatIconType } from './components/nav_control/onechat_nav_control'; export type { OnechatPluginSetup, OnechatPluginStart }; +export { onechatIconType }; export const plugin: PluginInitializer< OnechatPluginSetup, diff --git a/x-pack/solutions/security/plugins/security_solution/kibana.jsonc b/x-pack/solutions/security/plugins/security_solution/kibana.jsonc index 7b92c89286019..a68aaad898df9 100644 --- a/x-pack/solutions/security/plugins/security_solution/kibana.jsonc +++ b/x-pack/solutions/security/plugins/security_solution/kibana.jsonc @@ -91,7 +91,8 @@ "lists", "ml", "unifiedSearch", - "esql" + "esql", + "onechat" ], "extraPublicDirs": [ "common" diff --git a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx index f7863e96216ee..55b1c3a944672 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/agent_builder/components/new_agent_builder_attachment.tsx @@ -9,6 +9,7 @@ import type { EuiButtonColor, IconType } from '@elastic/eui'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import React, { memo } from 'react'; import type { EuiButtonEmptySizes } from '@elastic/eui/src/components/button/button_empty/button_empty'; +import { onechatIconType } from '@kbn/onechat-plugin/public'; import * as i18n from './translations'; export interface NewAgentBuilderAttachmentProps { @@ -41,7 +42,7 @@ export interface NewAgentBuilderAttachmentProps { */ export const NewAgentBuilderAttachment = memo(function NewAgentBuilderAttachment({ color = 'primary', - iconType = 'machineLearningApp', + iconType = onechatIconType, onClick, size = 'm', text = i18n.VIEW_IN_AGENT_BUILDER, From b49a60fbf0b79a1c4a5cd497d0c42792379ecdf8 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 14:53:43 +0100 Subject: [PATCH 93/96] better entity tool --- .../tools/entity_risk_score_tool.ts | 155 +++++++++++++++++- 1 file changed, 149 insertions(+), 6 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts index 9161cc98552e6..5853e434494e5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.ts @@ -11,6 +11,7 @@ import { ToolType, ToolResultType } from '@kbn/onechat-common'; import type { BuiltinToolDefinition, ToolAvailabilityContext } from '@kbn/onechat-server'; import { getToolResultId } from '@kbn/onechat-server/tools'; import { IdentifierType } from '../../../common/api/entity_analytics/common/common.gen'; +import type { EntityRiskScoreRecord } from '../../../common/api/entity_analytics/common'; import { createGetRiskScores } from '../../lib/entity_analytics/risk_score/get_risk_score'; import type { EntityType } from '../../../common/entity_analytics/types'; import { DEFAULT_ALERTS_INDEX, ESSENTIAL_ALERT_FIELDS } from '../../../common/constants'; @@ -23,11 +24,67 @@ const entityRiskScoreSchema = z.object({ identifier: z .string() .min(1) - .describe('The value that identifies the entity (e.g., hostname, username)'), + .describe( + 'The value that identifies the entity (e.g., hostname, username). Use "*" to get all entities of the specified type, sorted by risk score (highest first).' + ), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .describe('Maximum number of results to return when using wildcard queries (default: 10)'), }); export const SECURITY_ENTITY_RISK_SCORE_TOOL_ID = securityTool('entity_risk_score'); +/** + * Queries the risk index directly for wildcard queries, returning entities sorted by calculated_score_norm + */ +const queryRiskIndexForWildcard = async ({ + esClient, + spaceId, + entityType, + limit = 10, +}: { + esClient: ElasticsearchClient; + spaceId: string; + entityType: EntityType; + limit: number; +}): Promise => { + const riskIndex = getRiskIndex(spaceId, true); + const riskField = `${entityType}.risk.calculated_score_norm`; + + const response = await esClient.search>({ + index: riskIndex, + ignore_unavailable: true, + allow_no_indices: true, + size: limit, + query: { + bool: { + filter: [ + { + exists: { + field: `${entityType}.risk`, + }, + }, + ], + }, + }, + sort: [ + { + [riskField]: { + order: 'desc', + }, + }, + ], + }); + + return response.hits.hits + .map((hit) => (hit._source ? hit._source[entityType]?.risk : undefined)) + .filter((risk): risk is EntityRiskScoreRecord => risk !== undefined); +}; + /** * Fetches alerts by their IDs, returning only essential fields for risk score context */ @@ -71,7 +128,7 @@ export const entityRiskScoreTool = ( return { id: SECURITY_ENTITY_RISK_SCORE_TOOL_ID, type: ToolType.builtin, - description: `Call this tool to get the latest entity risk score and the inputs that contributed to the calculation for a specific entity (host, user, service, or generic). The risk score is sorted by 'kibana.alert.risk_score'. When reporting the risk score value, use the normalized field 'calculated_score_norm' which ranges from 0-100.`, + description: `Call this tool to get the latest entity risk score and the inputs that contributed to the calculation for a specific entity (host, user, service, or generic). Use identifier "*" to get all entities of the specified type sorted by risk score. IMPORTANT: Always use 'calculated_score_norm' (0-100) when reporting risk scores, NOT 'calculated_score' which is a raw value. The 'calculated_score_norm' field is the normalized score suitable for comparison between entities.`, schema: entityRiskScoreSchema, availability: { cacheMode: 'space', @@ -103,23 +160,88 @@ export const entityRiskScoreTool = ( } }, }, - handler: async ({ identifierType, identifier }, { request, esClient, logger }) => { + handler: async ({ identifierType, identifier, limit = 10 }, { request, esClient, logger }) => { const spaceId = getSpaceIdFromRequest(request); const alertsIndexPattern = `${DEFAULT_ALERTS_INDEX}-${spaceId}`; + const entityType = identifierType as EntityType; logger.debug( `${SECURITY_ENTITY_RISK_SCORE_TOOL_ID} tool called with identifierType: ${identifierType}, identifier: ${identifier}` ); try { + let riskScores: EntityRiskScoreRecord[]; + + // Handle wildcard queries by querying the risk index directly + if (identifier === '*') { + riskScores = await queryRiskIndexForWildcard({ + esClient: esClient.asCurrentUser, + spaceId, + entityType, + limit, + }); + + if (riskScores.length === 0) { + return { + results: [ + { + tool_result_id: getToolResultId(), + type: ToolResultType.error, + data: { + message: `No risk scores found for ${identifierType} entities`, + }, + }, + ], + }; + } + + // For wildcard queries, return all results without alert details (inputs) to avoid excessive data + // Reorder fields to prioritize calculated_score_norm + return { + results: [ + { + tool_result_id: getToolResultId(), + type: ToolResultType.other, + data: { + riskScores: riskScores.map((score) => { + // Exclude inputs and category details to reduce payload size when returning multiple entities + // Only include calculated_score_norm for clear prioity + return { + calculated_score_norm: score.calculated_score_norm, + calculated_level: score.calculated_level, + id_value: score.id_value, + id_field: score.id_field, + '@timestamp': score['@timestamp'], + ...(score.notes.length > 0 && { notes: score.notes }), + ...(score.criticality_modifier !== undefined && { + criticality_modifier: score.criticality_modifier, + }), + ...(score.criticality_level !== undefined && { + criticality_level: score.criticality_level, + }), + ...(score.is_privileged_user !== undefined && { + is_privileged_user: score.is_privileged_user, + }), + ...(score.privileged_user_modifier !== undefined && { + privileged_user_modifier: score.privileged_user_modifier, + }), + }; + }), + }, + }, + ], + }; + } + + // Handle specific entity queries const getRiskScore = createGetRiskScores({ logger, esClient: esClient.asCurrentUser, spaceId, }); - const riskScores = await getRiskScore({ - entityType: identifierType as EntityType, + riskScores = await getRiskScore({ + entityType, entityIdentifier: identifier, pagination: { querySize: 1, cursorStart: 0 }, }); @@ -156,9 +278,30 @@ export const entityRiskScoreTool = ( alert_contribution: alertsById[input.id] || null, })); + // Prioritize calculated_score_norm in the response structure const riskScoreData = { - ...latestRiskScore, + // Put calculated_score_norm first to emphasize its importance + calculated_score_norm: latestRiskScore.calculated_score_norm, + calculated_level: latestRiskScore.calculated_level, + id_value: latestRiskScore.id_value, + id_field: latestRiskScore.id_field, + // Include calculated_score but after normalized score + calculated_score: latestRiskScore.calculated_score, inputs: enhancedInputs, + '@timestamp': latestRiskScore['@timestamp'], + ...(latestRiskScore.notes.length > 0 && { notes: latestRiskScore.notes }), + ...(latestRiskScore.criticality_modifier !== undefined && { + criticality_modifier: latestRiskScore.criticality_modifier, + }), + ...(latestRiskScore.criticality_level !== undefined && { + criticality_level: latestRiskScore.criticality_level, + }), + ...(latestRiskScore.is_privileged_user !== undefined && { + is_privileged_user: latestRiskScore.is_privileged_user, + }), + ...(latestRiskScore.privileged_user_modifier !== undefined && { + privileged_user_modifier: latestRiskScore.privileged_user_modifier, + }), }; return { From 43a2c38ba7a7e50a1cae9781d2c4859bbf50a152 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 14:56:24 +0100 Subject: [PATCH 94/96] update onechat limits --- packages/kbn-optimizer/limits.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 759c73f203e22..a6dbb18b8bdf9 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -122,7 +122,7 @@ pageLoadAssetSize: observabilityLogsExplorer: 4918 observabilityOnboarding: 12872 observabilityShared: 75115 - onechat: 25223 + onechat: 28638 osquery: 47422 painlessLab: 6299 presentationPanel: 11418 From 1b7fdc37bfca7d18308ff45d3dedd4fed8dbaf51 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 16:51:06 +0100 Subject: [PATCH 95/96] test fixing --- .../rule_status_failed_callout.test.tsx | 22 +- .../tools/entity_risk_score_tool.test.ts | 192 +++++++++++++++++- 2 files changed, 190 insertions(+), 24 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx index 42bc7ab86aab0..1b996861dd543 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx @@ -12,10 +12,9 @@ import { render } from '@testing-library/react'; import type { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring'; import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring'; import { RuleStatusFailedCallOut } from './rule_status_failed_callout'; -import { QueryClient, QueryClientProvider } from '@kbn/react-query'; import { chromeServiceMock } from '@kbn/core/public/mocks'; import { of } from 'rxjs'; -import { MockAssistantProviderComponent } from '../../../../common/mock/mock_assistant_provider'; +import { TestProviders } from '../../../../common/mock/test_providers'; jest.mock('../../../../common/lib/kibana'); @@ -23,27 +22,10 @@ const TEST_ID = 'ruleStatusFailedCallOut'; const DATE = '2022-01-27T15:03:31.176Z'; const MESSAGE = 'This rule is attempting to query data but...'; -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - logger: { - log: jest.fn(), - warn: jest.fn(), - error: () => {}, - }, -}); - const ContextWrapper: FC> = ({ children }) => { const chrome = chromeServiceMock.createStartContract(); chrome.getChromeStyle$.mockReturnValue(of('classic')); - return ( - - {children} - - ); + return {children}; }; describe('RuleStatusFailedCallOut', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts index ea90f63e128dd..742823dd71740 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts @@ -42,6 +42,18 @@ describe('entityRiskScoreTool', () => { expect(result.success).toBe(true); }); + it('validates schema with optional limit parameter', () => { + const validInput = { + identifierType: 'user', + identifier: '*', + limit: 5, + }; + + const result = tool.schema.safeParse(validInput); + + expect(result.success).toBe(true); + }); + it('rejects invalid identifierType', () => { const invalidInput = { identifierType: 'invalid', @@ -63,6 +75,30 @@ describe('entityRiskScoreTool', () => { expect(result.success).toBe(false); }); + + it('rejects limit below minimum', () => { + const invalidInput = { + identifierType: 'host', + identifier: '*', + limit: 0, + }; + + const result = tool.schema.safeParse(invalidInput); + + expect(result.success).toBe(false); + }); + + it('rejects limit above maximum', () => { + const invalidInput = { + identifierType: 'host', + identifier: '*', + limit: 101, + }; + + const result = tool.schema.safeParse(invalidInput); + + expect(result.success).toBe(false); + }); }); describe('availability', () => { @@ -110,14 +146,21 @@ describe('entityRiskScoreTool', () => { it('successfully fetches risk score with valid identifierType and identifier', async () => { const mockGetRiskScores = jest.fn().mockResolvedValue([ { - id: 'risk-1', + '@timestamp': '2023-01-01T00:00:00Z', + id_field: 'host.name', + id_value: 'hostname-1', calculated_score_norm: 75, + calculated_level: 'High', + calculated_score: 150, + category_1_score: 75, + category_1_count: 5, + notes: [], inputs: [ { id: 'alert-1', risk_score: 50, contribution_score: 25, - category: 'alerts', + category: 'category_1', }, ], }, @@ -149,6 +192,13 @@ describe('entityRiskScoreTool', () => { expect(result.results[0].type).toBe(ToolResultType.other); if (result.results[0].type === ToolResultType.other) { expect(result.results[0].data).toHaveProperty('riskScore'); + const riskScore = result.results[0].data.riskScore as Record; + // Verify calculated_score_norm is prioritized (first field) + expect(Object.keys(riskScore)[0]).toBe('calculated_score_norm'); + expect(riskScore.calculated_score_norm).toBe(75); + // Verify category scores/counts are NOT included + expect(riskScore).not.toHaveProperty('category_1_score'); + expect(riskScore).not.toHaveProperty('category_1_count'); } expect(mockGetRiskScores).toHaveBeenCalledWith({ entityType: 'host', @@ -175,14 +225,21 @@ describe('entityRiskScoreTool', () => { it('enhances inputs with alert data', async () => { const mockGetRiskScores = jest.fn().mockResolvedValue([ { - id: 'risk-1', + '@timestamp': '2023-01-01T00:00:00Z', + id_field: 'host.name', + id_value: 'hostname-1', calculated_score_norm: 75, + calculated_level: 'High', + calculated_score: 150, + category_1_score: 75, + category_1_count: 5, + notes: [], inputs: [ { id: 'alert-1', risk_score: 50, contribution_score: 25, - category: 'alerts', + category: 'category_1', }, ], }, @@ -219,6 +276,133 @@ describe('entityRiskScoreTool', () => { ); }); + it('handles wildcard query with identifier "*"', async () => { + const mockRiskScores = [ + { + '@timestamp': '2023-01-01T00:00:00Z', + id_field: 'user.name', + id_value: 'user1', + calculated_score_norm: 90, + calculated_level: 'Critical', + calculated_score: 200, + category_1_score: 90, + category_1_count: 10, + notes: [], + inputs: [], + }, + { + '@timestamp': '2023-01-01T00:00:00Z', + id_field: 'user.name', + id_value: 'user2', + calculated_score_norm: 75, + calculated_level: 'High', + calculated_score: 150, + category_1_score: 75, + category_1_count: 5, + notes: [], + inputs: [], + }, + ]; + + mockEsClient.asCurrentUser.search.mockResolvedValue({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + hits: mockRiskScores.map((score) => ({ + _id: `risk-${score.id_value}`, + _index: 'test-index', + _source: { + user: { + name: score.id_value, + risk: score, + }, + }, + })), + total: { value: 2, relation: 'eq' }, + }, + }); + + const result = await tool.handler( + { identifierType: 'user', identifier: '*', limit: 10 }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + expect(result.results).toHaveLength(1); + expect(result.results[0].type).toBe(ToolResultType.other); + if (result.results[0].type === ToolResultType.other) { + expect(result.results[0].data).toHaveProperty('riskScores'); + const riskScores = result.results[0].data.riskScores as Array>; + expect(riskScores).toHaveLength(2); + // Verify calculated_score_norm is prioritized (first field) + expect(Object.keys(riskScores[0])[0]).toBe('calculated_score_norm'); + expect(riskScores[0].calculated_score_norm).toBe(90); + // Verify category scores/counts are NOT included + expect(riskScores[0]).not.toHaveProperty('category_1_score'); + expect(riskScores[0]).not.toHaveProperty('category_1_count'); + // Verify inputs are NOT included + expect(riskScores[0]).not.toHaveProperty('inputs'); + // Verify calculated_score is NOT included (user removed it) + expect(riskScores[0]).not.toHaveProperty('calculated_score'); + } + expect(mockEsClient.asCurrentUser.search).toHaveBeenCalledWith( + expect.objectContaining({ + index: getRiskIndex('default', true), + size: 10, + sort: expect.arrayContaining([ + expect.objectContaining({ + 'user.risk.calculated_score_norm': expect.objectContaining({ order: 'desc' }), + }), + ]), + }) + ); + }); + + it('handles wildcard query with custom limit', async () => { + mockEsClient.asCurrentUser.search.mockResolvedValue({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + hits: [], + total: { value: 0, relation: 'eq' }, + }, + }); + + const result = await tool.handler( + { identifierType: 'host', identifier: '*', limit: 5 }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + expect(mockEsClient.asCurrentUser.search).toHaveBeenCalledWith( + expect.objectContaining({ + size: 5, + }) + ); + }); + + it('returns error when wildcard query finds no results', async () => { + mockEsClient.asCurrentUser.search.mockResolvedValue({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + hits: [], + total: { value: 0, relation: 'eq' }, + }, + }); + + const result = await tool.handler( + { identifierType: 'user', identifier: '*' }, + createToolHandlerContext(mockRequest, mockEsClient, mockLogger) + ); + + expect(result.results).toHaveLength(1); + const errorResult = result.results[0] as ErrorResult; + expect(errorResult.type).toBe(ToolResultType.error); + expect(errorResult.data.message).toContain('No risk scores found for user entities'); + }); + it('handles ES client failures', async () => { const mockGetRiskScores = jest.fn().mockRejectedValue(new Error('ES error')); mockCreateGetRiskScores.mockReturnValue(mockGetRiskScores); From 3babba1858ad0e9b21ae0403f81dfb1c02be127a Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 3 Dec 2025 16:56:31 +0100 Subject: [PATCH 96/96] fix type --- .../tools/entity_risk_score_tool.test.ts | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts index 742823dd71740..4d65e2b785cde 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/agent_builder/tools/entity_risk_score_tool.test.ts @@ -190,16 +190,15 @@ describe('entityRiskScoreTool', () => { expect(result.results).toHaveLength(1); expect(result.results[0].type).toBe(ToolResultType.other); - if (result.results[0].type === ToolResultType.other) { - expect(result.results[0].data).toHaveProperty('riskScore'); - const riskScore = result.results[0].data.riskScore as Record; - // Verify calculated_score_norm is prioritized (first field) - expect(Object.keys(riskScore)[0]).toBe('calculated_score_norm'); - expect(riskScore.calculated_score_norm).toBe(75); - // Verify category scores/counts are NOT included - expect(riskScore).not.toHaveProperty('category_1_score'); - expect(riskScore).not.toHaveProperty('category_1_count'); - } + const otherResult = result.results[0] as OtherResult; + expect(otherResult.data).toHaveProperty('riskScore'); + const riskScore = (otherResult.data as { riskScore: Record }).riskScore; + // Verify calculated_score_norm is prioritized (first field) + expect(Object.keys(riskScore)[0]).toBe('calculated_score_norm'); + expect(riskScore.calculated_score_norm).toBe(75); + // Verify category scores/counts are NOT included + expect(riskScore).not.toHaveProperty('category_1_score'); + expect(riskScore).not.toHaveProperty('category_1_count'); expect(mockGetRiskScores).toHaveBeenCalledWith({ entityType: 'host', entityIdentifier: 'hostname-1', @@ -330,21 +329,21 @@ describe('entityRiskScoreTool', () => { expect(result.results).toHaveLength(1); expect(result.results[0].type).toBe(ToolResultType.other); - if (result.results[0].type === ToolResultType.other) { - expect(result.results[0].data).toHaveProperty('riskScores'); - const riskScores = result.results[0].data.riskScores as Array>; - expect(riskScores).toHaveLength(2); - // Verify calculated_score_norm is prioritized (first field) - expect(Object.keys(riskScores[0])[0]).toBe('calculated_score_norm'); - expect(riskScores[0].calculated_score_norm).toBe(90); - // Verify category scores/counts are NOT included - expect(riskScores[0]).not.toHaveProperty('category_1_score'); - expect(riskScores[0]).not.toHaveProperty('category_1_count'); - // Verify inputs are NOT included - expect(riskScores[0]).not.toHaveProperty('inputs'); - // Verify calculated_score is NOT included (user removed it) - expect(riskScores[0]).not.toHaveProperty('calculated_score'); - } + const otherResult = result.results[0] as OtherResult; + expect(otherResult.data).toHaveProperty('riskScores'); + const riskScores = (otherResult.data as { riskScores: Array> }) + .riskScores; + expect(riskScores).toHaveLength(2); + // Verify calculated_score_norm is prioritized (first field) + expect(Object.keys(riskScores[0])[0]).toBe('calculated_score_norm'); + expect(riskScores[0].calculated_score_norm).toBe(90); + // Verify category scores/counts are NOT included + expect(riskScores[0]).not.toHaveProperty('category_1_score'); + expect(riskScores[0]).not.toHaveProperty('category_1_count'); + // Verify inputs are NOT included + expect(riskScores[0]).not.toHaveProperty('inputs'); + // Verify calculated_score is NOT included (user removed it) + expect(riskScores[0]).not.toHaveProperty('calculated_score'); expect(mockEsClient.asCurrentUser.search).toHaveBeenCalledWith( expect.objectContaining({ index: getRiskIndex('default', true), @@ -369,7 +368,7 @@ describe('entityRiskScoreTool', () => { }, }); - const result = await tool.handler( + await tool.handler( { identifierType: 'host', identifier: '*', limit: 5 }, createToolHandlerContext(mockRequest, mockEsClient, mockLogger) );