From c062a77e8a2e5a1dd128cdb0b5237bf99d312924 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 23 Jun 2025 15:27:50 -0600 Subject: [PATCH 01/18] wip --- .../assistant/assistant_body/empty_convo.tsx | 18 ++++- .../impl/assistant/assistant_body/index.tsx | 2 +- .../assistant_body/starter_prompts.tsx | 78 +++++++++++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx index 86bd31318349c..1c16e68e8d49e 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx @@ -10,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { css } from '@emotion/react'; import { PromptResponse } from '@kbn/elastic-assistant-common'; import { AssistantBeacon } from '@kbn/ai-assistant-icon'; +import { StarterPrompts } from './starter_prompts'; import { SystemPrompt } from '../prompt_editor/system_prompt'; import { SetupKnowledgeBaseButton } from '../../knowledge_base/setup_knowledge_base_button'; import * as i18n from '../translations'; @@ -21,6 +22,9 @@ interface Props { setCurrentSystemPromptId: (promptId: string | undefined) => void; allSystemPrompts: PromptResponse[]; } +const starterPromptWrapperClassName = css` + max-width: 95%; +`; export const EmptyConvo: React.FC = ({ allSystemPrompts, @@ -30,7 +34,12 @@ export const EmptyConvo: React.FC = ({ setIsSettingsModalVisible, }) => { return ( - + = ({ + + { + console.log('prompt', p); + }} + /> + ); }; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx index f93b62bbe67eb..7330dc855127c 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx @@ -109,7 +109,7 @@ export const AssistantBody: FunctionComponent = ({ return ( - + {isLoading ? ( void; +} +const starterPromptClassName = css` + max-width: 50%; + min-width: calc(50% - 8px); +`; + +const starterPromptInnerClassName = css` + text-align: center !important; +`; + +export const StarterPrompts: React.FC = ({ onSelectPrompt }) => { + const starterPrompts = [ + { + prompt: 'What is the status of my last deployment?', + title: 'Deployment Status', + icon: 'sparkles', + }, + { + prompt: 'How many users logged in last week?', + title: 'User Activity', + icon: 'bell', + }, + { + prompt: 'What are the top errors in the logs?', + title: 'Log Analysis', + icon: 'bullseye', + }, + { + prompt: 'Can you summarize the latest sales report?', + title: 'Sales Summary', + icon: 'questionInCircle', + }, + ]; + return ( + + {starterPrompts.map(({ prompt, title, icon }) => ( + + onSelectPrompt(prompt)} + className={starterPromptInnerClassName} + > + + + + +

{title}

+
+ {prompt} +
+
+ ))} +
+ ); +}; From d7e209885c5b26bf15bd22665a10b11979451cc7 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 23 Jun 2025 15:33:34 -0600 Subject: [PATCH 02/18] more wip --- .../impl/assistant/assistant_body/starter_prompts.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx index b842328054f4b..f04e25dca7e09 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx @@ -31,16 +31,16 @@ const starterPromptInnerClassName = css` export const StarterPrompts: React.FC = ({ onSelectPrompt }) => { const starterPrompts = [ + { + prompt: 'Show me the important alerts from the last 24 hours', + title: 'User Activity', + icon: 'bell', + }, { prompt: 'What is the status of my last deployment?', title: 'Deployment Status', icon: 'sparkles', }, - { - prompt: 'How many users logged in last week?', - title: 'User Activity', - icon: 'bell', - }, { prompt: 'What are the top errors in the logs?', title: 'Log Analysis', From 82649427a5cf4eb392d67a3f3d0b1740257c9574 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 23 Jun 2025 16:02:21 -0600 Subject: [PATCH 03/18] ui works! --- .../assistant/assistant_body/empty_convo.tsx | 8 ++--- .../impl/assistant/assistant_body/index.tsx | 3 ++ .../assistant_body/starter_prompts.tsx | 31 +++++++++++++------ .../impl/assistant/index.tsx | 1 + 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx index 1c16e68e8d49e..37e0ec416ffa7 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx @@ -21,6 +21,7 @@ interface Props { setIsSettingsModalVisible: Dispatch>; setCurrentSystemPromptId: (promptId: string | undefined) => void; allSystemPrompts: PromptResponse[]; + setUserPrompt: React.Dispatch>; } const starterPromptWrapperClassName = css` max-width: 95%; @@ -32,6 +33,7 @@ export const EmptyConvo: React.FC = ({ isSettingsModalVisible, setCurrentSystemPromptId, setIsSettingsModalVisible, + setUserPrompt, }) => { return ( = ({
- { - console.log('prompt', p); - }} - /> +
); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx index 7330dc855127c..2f2bcef847b5e 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx @@ -43,6 +43,7 @@ interface Props { http: HttpSetup; setCurrentSystemPromptId: (promptId: string | undefined) => void; setIsSettingsModalVisible: Dispatch>; + setUserPrompt: React.Dispatch>; } export const AssistantBody: FunctionComponent = ({ @@ -58,6 +59,7 @@ export const AssistantBody: FunctionComponent = ({ isSettingsModalVisible, isWelcomeSetup, setIsSettingsModalVisible, + setUserPrompt, }) => { const { euiTheme } = useEuiTheme(); @@ -127,6 +129,7 @@ export const AssistantBody: FunctionComponent = ({ isSettingsModalVisible={isSettingsModalVisible} setCurrentSystemPromptId={setCurrentSystemPromptId} setIsSettingsModalVisible={setIsSettingsModalVisible} + setUserPrompt={setUserPrompt} /> ) : ( void; + setUserPrompt: React.Dispatch>; } const starterPromptClassName = css` max-width: 50%; @@ -29,33 +29,44 @@ const starterPromptInnerClassName = css` text-align: center !important; `; -export const StarterPrompts: React.FC = ({ onSelectPrompt }) => { +export const StarterPrompts: React.FC = ({ setUserPrompt }) => { const starterPrompts = [ { - prompt: 'Show me the important alerts from the last 24 hours', + description: 'Show me the important alerts from the last 24 hours', title: 'User Activity', icon: 'bell', + prompt: 'big long prompt 1', }, { - prompt: 'What is the status of my last deployment?', + description: 'What is the status of my last deployment?', title: 'Deployment Status', icon: 'sparkles', + prompt: 'big long prompt 2', }, { - prompt: 'What are the top errors in the logs?', + description: 'What are the top errors in the logs?', title: 'Log Analysis', icon: 'bullseye', + prompt: 'big long prompt 3', }, { - prompt: 'Can you summarize the latest sales report?', + description: 'Can you summarize the latest sales report?', title: 'Sales Summary', icon: 'questionInCircle', + prompt: 'big long prompt 4', }, ]; + + const onSelectPrompt = useCallback( + (prompt: string) => { + setUserPrompt(prompt); + }, + [setUserPrompt] + ); return ( - {starterPrompts.map(({ prompt, title, icon }) => ( - + {starterPrompts.map(({ description, title, icon, prompt }) => ( + = ({ onSelectPrompt }) => {

{title}

- {prompt} + {description}
))} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/index.tsx index dc9ad85039906..a3dd19ff7f4b7 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/index.tsx @@ -562,6 +562,7 @@ const AssistantComponent: React.FC = ({ isLoading={isInitialLoad} isSettingsModalVisible={isSettingsModalVisible} isWelcomeSetup={isWelcomeSetup} + setUserPrompt={setUserPrompt} setCurrentSystemPromptId={setCurrentSystemPromptId} setIsSettingsModalVisible={setIsSettingsModalVisible} /> From add9ccfb9bed671734690844da6fe5098a3c2665 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 23 Jun 2025 17:33:50 -0600 Subject: [PATCH 04/18] ui/api done --- .../api/prompts/use_fetch_prompts.test.tsx | 13 +- .../security_ai_prompts/use_find_prompts.ts | 4 +- .../assistant/assistant_body/empty_convo.tsx | 1 + .../assistant_body/starter_prompts.test.tsx | 161 ++++++++++++++++++ .../assistant_body/starter_prompts.tsx | 110 +++++++++--- .../server/lib/prompt/local_prompt_object.ts | 103 ++++++++++- .../tools/security_labs/security_labs_tool.ts | 4 + 7 files changed, 358 insertions(+), 38 deletions(-) create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.test.tsx diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx index 8fb1f808f8df5..fb934afcee437 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx @@ -11,14 +11,15 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { ReactNode } from 'react'; import React from 'react'; import { useFetchPrompts } from './use_fetch_prompts'; -import { HttpSetup } from '@kbn/core-http-browser'; import { useAssistantContext } from '../../../assistant_context'; -import { API_VERSIONS, defaultAssistantFeatures } from '@kbn/elastic-assistant-common'; - -const http = { - fetch: jest.fn().mockResolvedValue(defaultAssistantFeatures), -} as unknown as HttpSetup; +import { API_VERSIONS } from '@kbn/elastic-assistant-common'; +(useAssistantContext as jest.Mock).mockReturnValue({ + http, + assistantAvailability: { + isAssistantEnabled: true, + }, +}); jest.mock('../../../assistant_context'); const createWrapper = () => { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.ts index 00a93c99a2511..67a3604140b43 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.ts @@ -19,7 +19,7 @@ export interface UseFindPromptsParams { context: { isAssistantEnabled: boolean; httpFetch: HttpHandler; - toasts: IToasts; + toasts?: IToasts; }; signal?: AbortSignal | undefined; params: FindSecurityAIPromptsRequestQuery; @@ -81,7 +81,7 @@ const getPrompts = async ({ query, }: { httpFetch: HttpHandler; - toasts: IToasts; + toasts?: IToasts; signal?: AbortSignal | undefined; query: FindSecurityAIPromptsRequestQuery; }) => { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx index 37e0ec416ffa7..4c0e74de69ac7 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx @@ -75,6 +75,7 @@ export const EmptyConvo: React.FC = ({
+ {/* TODO feature flag */} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.test.tsx new file mode 100644 index 0000000000000..57064176c0417 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.test.tsx @@ -0,0 +1,161 @@ +/* + * 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 { + formatPromptGroups, + getAllPromptIds, + promptGroups, + StarterPrompts, +} from './starter_prompts'; +import { fireEvent, render } from '@testing-library/react'; +import { TestProviders } from '../../mock/test_providers/test_providers'; +import React from 'react'; +import { useFindPrompts } from '../../..'; +const mockResponse = [ + { + promptId: 'starterPromptTitle1', + prompt: 'starterPromptTitle1 from API yall', + }, + { + promptId: 'starterPromptDescription1', + prompt: 'starterPromptDescription1 from API yall', + }, + { + promptId: 'starterPromptIcon1', + prompt: 'starterPromptIcon1 from API yall', + }, + { + promptId: 'starterPromptPrompt1', + prompt: 'starterPromptPrompt1 from API yall', + }, + { + promptId: 'starterPromptDescription2', + prompt: 'starterPromptDescription2 from API yall', + }, + { + promptId: 'starterPromptTitle2', + prompt: 'starterPromptTitle2 from API yall', + }, + { + promptId: 'starterPromptIcon2', + prompt: 'starterPromptIcon2 from API yall', + }, + { + promptId: 'starterPromptPrompt2', + prompt: 'starterPromptPrompt2 from API yall', + }, + { + promptId: 'starterPromptDescription3', + prompt: 'starterPromptDescription3 from API yall', + }, + { + promptId: 'starterPromptTitle3', + prompt: 'starterPromptTitle3 from API yall', + }, + { + promptId: 'starterPromptIcon3', + prompt: 'starterPromptIcon3 from API yall', + }, + { + promptId: 'starterPromptPrompt3', + prompt: 'starterPromptPrompt3 from API yall', + }, + { + promptId: 'starterPromptDescription4', + prompt: 'starterPromptDescription4 from API yall', + }, + { + promptId: 'starterPromptTitle4', + prompt: 'starterPromptTitle4 from API yall', + }, + { + promptId: 'starterPromptPrompt4', + prompt: 'starterPromptPrompt4 from API yall', + }, +]; + +const testProps = { + setUserPrompt: jest.fn(), +}; + +jest.mock('../../..', () => { + return { + useFindPrompts: jest.fn(), + useAssistantContext: () => ({ + assistantAvailability: { + isAssistantEnabled: true, + }, + http: { fetch: {} }, + }), + }; +}); + +describe('StarterPrompts', () => { + it('should return an empty array if no prompts are provided', () => { + expect(getAllPromptIds(promptGroups)).toEqual([ + 'starterPromptTitle1', + 'starterPromptDescription1', + 'starterPromptIcon1', + 'starterPromptPrompt1', + 'starterPromptDescription2', + 'starterPromptTitle2', + 'starterPromptIcon2', + 'starterPromptPrompt2', + 'starterPromptDescription3', + 'starterPromptTitle3', + 'starterPromptIcon3', + 'starterPromptPrompt3', + 'starterPromptDescription4', + 'starterPromptTitle4', + 'starterPromptIcon4', + 'starterPromptPrompt4', + ]); + }); + it('should return the correct prompt groups with fetched prompts', () => { + const response = formatPromptGroups(mockResponse); + expect(response).toEqual([ + { + description: 'starterPromptDescription1 from API yall', + icon: 'starterPromptIcon1 from API yall', + prompt: 'starterPromptPrompt1 from API yall', + title: 'starterPromptTitle1 from API yall', + }, + { + description: 'starterPromptDescription2 from API yall', + icon: 'starterPromptIcon2 from API yall', + prompt: 'starterPromptPrompt2 from API yall', + title: 'starterPromptTitle2 from API yall', + }, + { + description: 'starterPromptDescription3 from API yall', + icon: 'starterPromptIcon3 from API yall', + prompt: 'starterPromptPrompt3 from API yall', + title: 'starterPromptTitle3 from API yall', + }, + // starterPrompt Group4 should not exist because starterPromptIcon4 is not in the mockResponse + ]); + }); + it('the component renders correctly with valid props', () => { + (useFindPrompts as jest.Mock).mockReturnValue({ data: { prompts: mockResponse } }); + const { getByTestId } = render( + + + + ); + expect(getByTestId('starterPromptPrompt2 from API yall')).toBeInTheDocument(); + }); + it('calls setUserPrompt when a prompt is selected', () => { + (useFindPrompts as jest.Mock).mockReturnValue({ data: { prompts: mockResponse } }); + const { getByTestId } = render( + + + + ); + fireEvent.click(getByTestId('starterPromptPrompt2 from API yall')); + expect(testProps.setUserPrompt).toHaveBeenCalledWith('starterPromptPrompt2 from API yall'); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx index f9c0bbd1284df..dd05a42ad3284 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/starter_prompts.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -16,6 +16,8 @@ import { EuiTitle, } from '@elastic/eui'; import { css } from '@emotion/css'; +import { PromptItemArray } from '@kbn/elastic-assistant-common/impl/schemas/security_ai_prompts/common_attributes.gen'; +import { useAssistantContext, useFindPrompts } from '../../..'; interface Props { setUserPrompt: React.Dispatch>; @@ -28,34 +30,66 @@ const starterPromptClassName = css` const starterPromptInnerClassName = css` text-align: center !important; `; +interface PromptGroup { + description: string; + title: string; + icon: string; + prompt: string; +} +// these are the promptIds (Security AI Prompts integration) for each of the starter prompts fields +export const promptGroups = [ + { + title: 'starterPromptTitle1', + description: 'starterPromptDescription1', + icon: 'starterPromptIcon1', + prompt: 'starterPromptPrompt1', + }, + { + description: 'starterPromptDescription2', + title: 'starterPromptTitle2', + icon: 'starterPromptIcon2', + prompt: 'starterPromptPrompt2', + }, + { + description: 'starterPromptDescription3', + title: 'starterPromptTitle3', + icon: 'starterPromptIcon3', + prompt: 'starterPromptPrompt3', + }, + { + description: 'starterPromptDescription4', + title: 'starterPromptTitle4', + icon: 'starterPromptIcon4', + prompt: 'starterPromptPrompt4', + }, +]; export const StarterPrompts: React.FC = ({ setUserPrompt }) => { - const starterPrompts = [ - { - description: 'Show me the important alerts from the last 24 hours', - title: 'User Activity', - icon: 'bell', - prompt: 'big long prompt 1', - }, - { - description: 'What is the status of my last deployment?', - title: 'Deployment Status', - icon: 'sparkles', - prompt: 'big long prompt 2', + const { + assistantAvailability: { isAssistantEnabled }, + http, + toasts, + } = useAssistantContext(); + const { + data: { prompts: actualPrompts }, + } = useFindPrompts({ + context: { + isAssistantEnabled, + httpFetch: http.fetch, + toasts, }, - { - description: 'What are the top errors in the logs?', - title: 'Log Analysis', - icon: 'bullseye', - prompt: 'big long prompt 3', + params: { + prompt_group_id: 'aiAssistant', + prompt_ids: getAllPromptIds(promptGroups), }, - { - description: 'Can you summarize the latest sales report?', - title: 'Sales Summary', - icon: 'questionInCircle', - prompt: 'big long prompt 4', - }, - ]; + }); + + const fetchedPromptGroups = useMemo(() => { + if (!actualPrompts.length) { + return []; + } + return formatPromptGroups(actualPrompts); + }, [actualPrompts]); const onSelectPrompt = useCallback( (prompt: string) => { @@ -63,14 +97,16 @@ export const StarterPrompts: React.FC = ({ setUserPrompt }) => { }, [setUserPrompt] ); + return ( - {starterPrompts.map(({ description, title, icon, prompt }) => ( - + {fetchedPromptGroups.map(({ description, title, icon, prompt }) => ( + onSelectPrompt(prompt)} className={starterPromptInnerClassName} > @@ -87,3 +123,23 @@ export const StarterPrompts: React.FC = ({ setUserPrompt }) => { ); }; + +export const getAllPromptIds = (pGroups: PromptGroup[]) => { + return pGroups.map((promptGroup: PromptGroup) => [...Object.values(promptGroup)]).flat(); +}; + +export const formatPromptGroups = (actualPrompts: PromptItemArray): PromptGroup[] => + promptGroups.reduce((acc, promptGroup) => { + const foundPrompt = (field: keyof PromptGroup) => + actualPrompts.find((p) => p.promptId === promptGroup[field])?.prompt; + const toBePrompt = { + prompt: foundPrompt('prompt'), + icon: foundPrompt('icon'), + title: foundPrompt('title'), + description: foundPrompt('description'), + }; + if (toBePrompt.prompt && toBePrompt.icon && toBePrompt.title && toBePrompt.description) { + acc.push(toBePrompt as PromptGroup); + } + return acc; + }, []); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts index 703e29c8f30a4..2ba27a46a1a65 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts @@ -41,9 +41,6 @@ export const promptGroupId = { }; export const promptDictionary = { - alertEvaluation: `alertEvaluation`, - dataQualityAnalysis: 'dataQualityAnalysis', - ruleAnalysis: 'ruleAnalysis', alertSummary: `alertSummary`, alertSummarySystemPrompt: `alertSummarySystemPrompt`, systemPrompt: `systemPrompt`, @@ -68,6 +65,26 @@ export const promptDictionary = { 'defendInsights-incompatibleAntivirusEventsEndpointId', defendInsightsIncompatibleAntivirusEventsValue: 'defendInsights-incompatibleAntivirusEventsValue', // context prompts + alertEvaluation: `alertEvaluation`, + dataQualityAnalysis: 'dataQualityAnalysis', + ruleAnalysis: 'ruleAnalysis', + // starter prompts + starterPromptDescription1: 'starterPromptDescription1', + starterPromptTitle1: 'starterPromptTitle1', + starterPromptIcon1: 'starterPromptIcon1', + starterPromptPrompt1: 'starterPromptPrompt1', + starterPromptDescription2: 'starterPromptDescription2', + starterPromptTitle2: 'starterPromptTitle2', + starterPromptIcon2: 'starterPromptIcon2', + starterPromptPrompt2: 'starterPromptPrompt2', + starterPromptDescription3: 'starterPromptDescription3', + starterPromptTitle3: 'starterPromptTitle3', + starterPromptIcon3: 'starterPromptIcon3', + starterPromptPrompt3: 'starterPromptPrompt3', + starterPromptDescription4: 'starterPromptDescription4', + starterPromptTitle4: 'starterPromptTitle4', + starterPromptIcon4: 'starterPromptIcon4', + starterPromptPrompt4: 'starterPromptPrompt4', }; export const localPrompts: Prompt[] = [ @@ -287,4 +304,84 @@ export const localPrompts: Prompt[] = [ default: RULE_ANALYSIS, }, }, + { + promptId: promptDictionary.starterPromptDescription1, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptDescription1 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptTitle1, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptTitle1 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptIcon1, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptIcon1 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptPrompt1, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptPrompt1 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptDescription2, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptDescription2 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptTitle2, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptTitle2 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptIcon2, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptIcon2 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptPrompt2, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptPrompt2 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptDescription3, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptDescription3 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptTitle3, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptTitle3 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptIcon3, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptIcon3 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptPrompt3, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptPrompt3 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptDescription4, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptDescription4 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptTitle4, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptTitle4 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptIcon4, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptIcon4 from API yall' }, + }, + { + promptId: promptDictionary.starterPromptPrompt4, + promptGroupId: promptGroupId.aiAssistant, + prompt: { default: 'starterPromptPrompt4 from API yall' }, + }, ]; diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts index d9f4f75cd7716..7ec1386a10ad5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts @@ -51,6 +51,10 @@ export const SECURITY_LABS_KNOWLEDGE_BASE_TOOL: AssistantTool = { kbResource: SECURITY_LABS_RESOURCE, query: input.question, }); + if (docs.length === 0) { + // TODO add KB install check here + return 'No relevant Elastic Security Labs content found for the provided query. Ensure knowledge base is installed.'; + } const citedDocs = docs.map((doc) => { let reference: ContentReference | undefined; From 057cb17a62eec18dda2299aee050173f691a09ec Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 23 Jun 2025 17:46:36 -0600 Subject: [PATCH 05/18] add feature flag --- .../assistant/assistant_body/empty_convo.tsx | 11 +++++--- .../impl/assistant_context/types.tsx | 2 ++ .../common/experimental_features.ts | 4 +++ .../use_assistant_availability/index.tsx | 26 ++++++------------- .../tools/security_labs/security_labs_tool.ts | 4 +-- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx index 4c0e74de69ac7..89965f52a9333 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx @@ -10,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { css } from '@emotion/react'; import { PromptResponse } from '@kbn/elastic-assistant-common'; import { AssistantBeacon } from '@kbn/ai-assistant-icon'; +import { useAssistantContext } from '../../..'; import { StarterPrompts } from './starter_prompts'; import { SystemPrompt } from '../prompt_editor/system_prompt'; import { SetupKnowledgeBaseButton } from '../../knowledge_base/setup_knowledge_base_button'; @@ -35,6 +36,7 @@ export const EmptyConvo: React.FC = ({ setIsSettingsModalVisible, setUserPrompt, }) => { + const { assistantAvailability } = useAssistantContext(); return ( = ({ - {/* TODO feature flag */} - - - + {assistantAvailability.isStarterPromptsEnabled && ( + + + + )} ); }; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx index 3c8ecbb2a70dc..ce024d7d02178 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx @@ -74,6 +74,8 @@ export interface AssistantAvailability { hasUpdateAIAssistantAnonymization: boolean; // When true, user has `Edit` privilege for `Global Knowledge Base` hasManageGlobalKnowledgeBase: boolean; + // remove once product has signed off on prompt text + isStarterPromptsEnabled: boolean; } export type GetAssistantMessages = (commentArgs: { 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 fe86441f86cd9..a5e7c9a8fcf72 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 @@ -279,6 +279,10 @@ export const allowedExperimentalValues = Object.freeze({ * Enables advanced mode for Trusted Apps creation and update */ trustedAppsAdvancedMode: false, + /** + * Enables the starter prompts in the Elastic Assistant + */ + starterPromptsEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx index e08aa8579c2ae..a88850db20948 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx @@ -5,28 +5,13 @@ * 2.0. */ +import type { AssistantAvailability } from '@kbn/elastic-assistant'; +import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { useLicense } from '../../common/hooks/use_license'; import { useKibana } from '../../common/lib/kibana'; import { ASSISTANT_FEATURE_ID, SECURITY_FEATURE_ID } from '../../../common/constants'; -export interface UseAssistantAvailability { - // True when searchAiLake configurations is available - hasSearchAILakeConfigurations: boolean; - // True when user is Enterprise. When false, the Assistant is disabled and unavailable - isAssistantEnabled: boolean; - // When true, the Assistant is hidden and unavailable - hasAssistantPrivilege: boolean; - // When true, user has `All` privilege for `Connectors and Actions` (show/execute/delete/save ui capabilities) - hasConnectorsAllPrivilege: boolean; - // When true, user has `Read` privilege for `Connectors and Actions` (show/execute ui capabilities) - hasConnectorsReadPrivilege: boolean; - // When true, user has `Edit` privilege for `AnonymizationFields` - hasUpdateAIAssistantAnonymization: boolean; - // When true, user has `Edit` privilege for `Global Knowledge Base` - hasManageGlobalKnowledgeBase: boolean; -} - -export const useAssistantAvailability = (): UseAssistantAvailability => { +export const useAssistantAvailability = (): AssistantAvailability => { const isEnterprise = useLicense().isEnterprise(); const capabilities = useKibana().services.application.capabilities; const hasAssistantPrivilege = capabilities[ASSISTANT_FEATURE_ID]?.['ai-assistant'] === true; @@ -46,12 +31,17 @@ export const useAssistantAvailability = (): UseAssistantAvailability => { capabilities.actions?.delete === true && capabilities.actions?.save === true; + const starterPromptsEnabled = useIsExperimentalFeatureEnabled('starterPromptsEnabled'); + // remove once product has signed off on prompt text + const isStarterPromptsEnabled = starterPromptsEnabled; + return { hasSearchAILakeConfigurations, hasAssistantPrivilege, hasConnectorsAllPrivilege, hasConnectorsReadPrivilege, isAssistantEnabled: isEnterprise, + isStarterPromptsEnabled, hasUpdateAIAssistantAnonymization, hasManageGlobalKnowledgeBase, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts index 7ec1386a10ad5..a543bd6dddd42 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts @@ -51,8 +51,8 @@ export const SECURITY_LABS_KNOWLEDGE_BASE_TOOL: AssistantTool = { kbResource: SECURITY_LABS_RESOURCE, query: input.question, }); - if (docs.length === 0) { - // TODO add KB install check here + if (docs.length === 0 && !params.isEnabledKnowledgeBase) { + // prompt to help user install knowledge base return 'No relevant Elastic Security Labs content found for the provided query. Ensure knowledge base is installed.'; } From 2b9942601180910f45955c9fe1b07d491a4018cc Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 23 Jun 2025 18:19:45 -0600 Subject: [PATCH 06/18] fix isKnowledgeBaseInstalled --- .../api/prompts/use_fetch_prompts.test.tsx | 13 ++-- .../server/lib/prompt/local_prompt_object.ts | 48 +++++++++----- .../server/lib/prompt/prompts.ts | 64 +++++++++++++++++++ .../tools/security_labs/security_labs_tool.ts | 7 +- 4 files changed, 107 insertions(+), 25 deletions(-) diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx index fb934afcee437..8fb1f808f8df5 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx @@ -11,15 +11,14 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { ReactNode } from 'react'; import React from 'react'; import { useFetchPrompts } from './use_fetch_prompts'; +import { HttpSetup } from '@kbn/core-http-browser'; import { useAssistantContext } from '../../../assistant_context'; -import { API_VERSIONS } from '@kbn/elastic-assistant-common'; +import { API_VERSIONS, defaultAssistantFeatures } from '@kbn/elastic-assistant-common'; + +const http = { + fetch: jest.fn().mockResolvedValue(defaultAssistantFeatures), +} as unknown as HttpSetup; -(useAssistantContext as jest.Mock).mockReturnValue({ - http, - assistantAvailability: { - isAssistantEnabled: true, - }, -}); jest.mock('../../../assistant_context'); const createWrapper = () => { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts index 2ba27a46a1a65..4e5e86ea73368 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts @@ -29,6 +29,22 @@ import { RULE_ANALYSIS, DATA_QUALITY_ANALYSIS, ALERT_EVALUATION, + starterPromptTitle1, + starterPromptDescription1, + starterPromptIcon1, + starterPromptPrompt1, + starterPromptDescription2, + starterPromptTitle2, + starterPromptIcon2, + starterPromptPrompt2, + starterPromptDescription3, + starterPromptTitle3, + starterPromptIcon3, + starterPromptPrompt3, + starterPromptDescription4, + starterPromptTitle4, + starterPromptIcon4, + starterPromptPrompt4, } from './prompts'; export const promptGroupId = { @@ -307,81 +323,81 @@ export const localPrompts: Prompt[] = [ { promptId: promptDictionary.starterPromptDescription1, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptDescription1 from API yall' }, + prompt: { default: starterPromptDescription1 }, }, { promptId: promptDictionary.starterPromptTitle1, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptTitle1 from API yall' }, + prompt: { default: starterPromptTitle1 }, }, { promptId: promptDictionary.starterPromptIcon1, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptIcon1 from API yall' }, + prompt: { default: starterPromptIcon1 }, }, { promptId: promptDictionary.starterPromptPrompt1, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptPrompt1 from API yall' }, + prompt: { default: starterPromptPrompt1 }, }, { promptId: promptDictionary.starterPromptDescription2, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptDescription2 from API yall' }, + prompt: { default: starterPromptDescription2 }, }, { promptId: promptDictionary.starterPromptTitle2, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptTitle2 from API yall' }, + prompt: { default: starterPromptTitle2 }, }, { promptId: promptDictionary.starterPromptIcon2, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptIcon2 from API yall' }, + prompt: { default: starterPromptIcon2 }, }, { promptId: promptDictionary.starterPromptPrompt2, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptPrompt2 from API yall' }, + prompt: { default: starterPromptPrompt2 }, }, { promptId: promptDictionary.starterPromptDescription3, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptDescription3 from API yall' }, + prompt: { default: starterPromptDescription3 }, }, { promptId: promptDictionary.starterPromptTitle3, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptTitle3 from API yall' }, + prompt: { default: starterPromptTitle3 }, }, { promptId: promptDictionary.starterPromptIcon3, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptIcon3 from API yall' }, + prompt: { default: starterPromptIcon3 }, }, { promptId: promptDictionary.starterPromptPrompt3, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptPrompt3 from API yall' }, + prompt: { default: starterPromptPrompt3 }, }, { promptId: promptDictionary.starterPromptDescription4, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptDescription4 from API yall' }, + prompt: { default: starterPromptDescription4 }, }, { promptId: promptDictionary.starterPromptTitle4, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptTitle4 from API yall' }, + prompt: { default: starterPromptTitle4 }, }, { promptId: promptDictionary.starterPromptIcon4, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptIcon4 from API yall' }, + prompt: { default: starterPromptIcon4 }, }, { promptId: promptDictionary.starterPromptPrompt4, promptGroupId: promptGroupId.aiAssistant, - prompt: { default: 'starterPromptPrompt4 from API yall' }, + prompt: { default: starterPromptPrompt4 }, }, ]; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts index 94ac4c2af1579..bf8b30a6b884a 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts @@ -255,3 +255,67 @@ Formatting Requirements: - Use concise, actionable language. - Include relevant emojis in section headers for visual clarity (e.g., 📝, 🛡️, 🔍, 📚). `; + +export const starterPromptTitle1 = 'Alerts'; +export const starterPromptDescription1 = 'Most important alerts from the last 24 hrs'; +export const starterPromptIcon1 = 'bell'; +export const starterPromptPrompt1 = `🔍 Identify and Prioritize Today's Most Critical Alerts +Provide a structured summary of today's most significant alerts, including: +🛡️ Critical Alerts Overview +Highlight the most impactful alerts based on risk scores, severity, and affected entities. +Summarize key details such as alert name, risk score, severity, and associated users or hosts. +📊 Risk Context +Include user and host risk scores for each alert to provide additional context. +Reference relevant MITRE ATT&CK techniques, with hyperlinks to the official MITRE pages. +🚨 Why These Alerts Matter +Explain why these alerts are critical, focusing on potential business impact, lateral movement risks, or sensitive data exposure. +🔧 Recommended Next Steps +Provide actionable triage steps for each alert, such as: +Investigating the alert in Elastic Security. +Reviewing related events in Timelines. +Analyzing user and host behavior using Entity Analytics. +Suggest Elastic Defend endpoint response actions (e.g., isolate host, kill process, retrieve/delete file), with links to Elastic documentation. +📚 Documentation and References +Include direct links to Elastic Security documentation and relevant MITRE ATT&CK pages for further guidance. +Use markdown headers, tables, and code blocks for clarity. Include relevant emojis for visual distinction and ensure the response is concise, actionable, and tailored to Elastic Security workflows.`; +export const starterPromptDescription2 = 'Latest Elastic Security Labs research'; +export const starterPromptTitle2 = 'Research'; +export const starterPromptIcon2 = 'launch'; +export const starterPromptPrompt2 = `Retrieve and summarize the latest Elastic Security Labs articles one by one sorted by latest at the top. Ensure the response includes: +Article Summaries +Title and Link: Provide the title of each article with a hyperlink to the original content. +Publication Date: Include the date the article was published. +Key Insights: Summarize the main points or findings of each article in concise bullet points. +Relevant Threats or Techniques: Highlight any specific malware, attack techniques, or adversary behaviors discussed, with references to MITRE ATT&CK techniques (include hyperlinks to the official MITRE pages). +Practical Applications +Detection and Response Guidance: Provide actionable steps or recommendations based on the article's content, tailored for Elastic Security workflows. +Elastic Security Features: Highlight any Elastic Security features, detection rules, or tools mentioned in the articles, with links to relevant documentation. +Example Queries: If applicable, include example ES|QL or OSQuery Manager queries inspired by the article's findings, formatted as code blocks. +Documentation and Resources +Elastic Security Labs: Include a link to the Elastic Security Labs homepage. +Additional References: Provide links to any related Elastic documentation or external resources mentioned in the articles. +Formatting Requirements +Use markdown headers, tables, and code blocks for clarity. +Organize the response into visually distinct sections. +Use concise, actionable language.`; +export const starterPromptDescription3 = 'Generate ES|QL Queries'; +export const starterPromptTitle3 = 'Query'; +export const starterPromptIcon3 = 'esqlVis'; +export const starterPromptPrompt3 = + 'I need an Elastic ES|QL query to achieve the following goal:\n' + + 'Goal/Requirement:\n' + + '\n' + + 'Please:\n' + + 'Generate the ES|QL Query: Provide a complete ES|QL query tailored to the stated goal.\n' + + 'Explain the Query: Offer a brief explanation of each part of the query, including filters, fields, and logic used.\n' + + 'Optimize for Elastic Security: Suggest additional filters, aggregations, or enhancements to make the query more efficient and actionable within Elastic Security workflows.\n' + + 'Provide Documentation Links: Include links to relevant Elastic Security documentation for deeper understanding.\n' + + 'Formatting Requirements:\n' + + 'Use code blocks for the ES|QL query.\n' + + 'Include concise explanations in bullet points for clarity.\n' + + 'Highlight any advanced ES|QL features used in the query.\n'; +export const starterPromptDescription4 = 'Discover the types of questions you can ask'; +export const starterPromptTitle4 = 'Suggest'; +export const starterPromptIcon4 = 'sparkles'; +export const starterPromptPrompt4 = + 'Can you provide examples of questions I can ask about Elastic Security, such as investigating alerts, running ES|QL queries, incident response, or threat intelligence?'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts index a543bd6dddd42..fb38c9666811c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts @@ -18,6 +18,7 @@ import { knowledgeBaseReference, } from '@kbn/elastic-assistant-common/impl/content_references/references'; import { Document } from 'langchain/document'; +import { getIsKnowledgeBaseInstalled } from '@kbn/elastic-assistant-plugin/server/routes/helpers'; import { APP_UI_ID } from '../../../../common'; const toolDetails = { @@ -51,9 +52,11 @@ export const SECURITY_LABS_KNOWLEDGE_BASE_TOOL: AssistantTool = { kbResource: SECURITY_LABS_RESOURCE, query: input.question, }); - if (docs.length === 0 && !params.isEnabledKnowledgeBase) { + // this value does not tell me if security labs exists + const isKnowledgeBaseInstalled = await getIsKnowledgeBaseInstalled(kbDataClient); + if (docs.length === 0 && !isKnowledgeBaseInstalled) { // prompt to help user install knowledge base - return 'No relevant Elastic Security Labs content found for the provided query. Ensure knowledge base is installed.'; + return 'The "AI Assistant knowledge base" needs to be installed, containing the Security Labs content. Navigate to the Knowledge Base page in the AI Assistant Settings to install it.'; } const citedDocs = docs.map((doc) => { From 71dadcbfc606e07a42ea93100aaa59c6343c41ac Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 Jun 2025 07:31:43 -0600 Subject: [PATCH 07/18] fix type --- .../impl/mock/test_providers/test_providers.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx index a37332b2cf68e..b8a68676a645b 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx @@ -38,6 +38,7 @@ export const mockAssistantAvailability: AssistantAvailability = { hasUpdateAIAssistantAnonymization: true, hasManageGlobalKnowledgeBase: true, isAssistantEnabled: true, + isStarterPromptsEnabled: true, }; /** A utility for wrapping children in the providers required to run tests */ From 4e4efa4e8365a9f588b1544de08c5ad8a663c319 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 Jun 2025 07:45:53 -0600 Subject: [PATCH 08/18] rm outdated comment --- .../server/assistant/tools/security_labs/security_labs_tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts index fb38c9666811c..47c3e5cebac64 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts @@ -52,7 +52,7 @@ export const SECURITY_LABS_KNOWLEDGE_BASE_TOOL: AssistantTool = { kbResource: SECURITY_LABS_RESOURCE, query: input.question, }); - // this value does not tell me if security labs exists + const isKnowledgeBaseInstalled = await getIsKnowledgeBaseInstalled(kbDataClient); if (docs.length === 0 && !isKnowledgeBaseInstalled) { // prompt to help user install knowledge base From 9530c0b1f9de5a295962bafa6d4cf48da637d7bb Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 Jun 2025 07:47:52 -0600 Subject: [PATCH 09/18] move check within docs.length 0 --- .../tools/security_labs/security_labs_tool.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts index 47c3e5cebac64..c37d1e4e73690 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts @@ -53,10 +53,12 @@ export const SECURITY_LABS_KNOWLEDGE_BASE_TOOL: AssistantTool = { query: input.question, }); - const isKnowledgeBaseInstalled = await getIsKnowledgeBaseInstalled(kbDataClient); - if (docs.length === 0 && !isKnowledgeBaseInstalled) { - // prompt to help user install knowledge base - return 'The "AI Assistant knowledge base" needs to be installed, containing the Security Labs content. Navigate to the Knowledge Base page in the AI Assistant Settings to install it.'; + if (docs.length === 0) { + const isKnowledgeBaseInstalled = await getIsKnowledgeBaseInstalled(kbDataClient); + if (!isKnowledgeBaseInstalled) { + // prompt to help user install knowledge base + return 'The "AI Assistant knowledge base" needs to be installed, containing the Security Labs content. Navigate to the Knowledge Base page in the AI Assistant Settings to install it.'; + } } const citedDocs = docs.map((doc) => { From 3b88071f8c4c4d56ad3e5618e91c059c32b52afc Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 Jun 2025 07:52:16 -0600 Subject: [PATCH 10/18] fix type in DQD code --- .../data_quality_panel/mock/test_providers/test_providers.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx index 704a3f501a8f7..2e65adf9273a9 100644 --- a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx +++ b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx @@ -55,6 +55,7 @@ const TestExternalProvidersComponent: React.FC = ({ hasUpdateAIAssistantAnonymization: true, hasManageGlobalKnowledgeBase: true, isAssistantEnabled: true, + isStarterPromptsEnabled: true, }; const queryClient = new QueryClient({ defaultOptions: { From 7325e0c9dbae87ec5470813b0bd3376667664f20 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 Jun 2025 08:02:28 -0600 Subject: [PATCH 11/18] fix types --- .../public/common/mock/mock_assistant_provider.tsx | 1 + .../flyout/document_details/right/hooks/use_assistant.test.tsx | 2 ++ 2 files changed, 3 insertions(+) diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx index 29965d1bc61ff..8a07f874bc11e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx @@ -39,6 +39,7 @@ export const MockAssistantProviderComponent: React.FC = ({ hasUpdateAIAssistantAnonymization: true, hasManageGlobalKnowledgeBase: true, isAssistantEnabled: true, + isStarterPromptsEnabled: true, }; const chrome = chromeServiceMock.createStartContract(); chrome.getChromeStyle$.mockReturnValue(of('classic')); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx index 66f16c9280923..eac7230c2dad7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx @@ -36,6 +36,7 @@ describe('useAssistant', () => { hasUpdateAIAssistantAnonymization: true, hasManageGlobalKnowledgeBase: true, isAssistantEnabled: true, + isStarterPromptsEnabled: true, }); useAssistantOverlayMock.mockReturnValue({ showAssistantOverlay: jest.fn, @@ -88,6 +89,7 @@ describe('useAssistant', () => { hasUpdateAIAssistantAnonymization: true, hasManageGlobalKnowledgeBase: true, isAssistantEnabled: true, + isStarterPromptsEnabled: true, }); hookResult = renderUseAssistant(); From 8648778b7ac818c70092b879dd95f5c875e7ba2d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 Jun 2025 08:13:37 -0600 Subject: [PATCH 12/18] add connector_id to request --- .../impl/assistant/assistant_body/empty_convo.tsx | 4 +++- .../impl/assistant/assistant_body/index.tsx | 1 + .../impl/assistant/assistant_body/starter_prompts.tsx | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx index 89965f52a9333..ef2b75e86d64f 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/empty_convo.tsx @@ -17,6 +17,7 @@ import { SetupKnowledgeBaseButton } from '../../knowledge_base/setup_knowledge_b import * as i18n from '../translations'; interface Props { + connectorId?: string; currentSystemPromptId: string | undefined; isSettingsModalVisible: boolean; setIsSettingsModalVisible: Dispatch>; @@ -30,6 +31,7 @@ const starterPromptWrapperClassName = css` export const EmptyConvo: React.FC = ({ allSystemPrompts, + connectorId, currentSystemPromptId, isSettingsModalVisible, setCurrentSystemPromptId, @@ -79,7 +81,7 @@ export const EmptyConvo: React.FC = ({ {assistantAvailability.isStarterPromptsEnabled && ( - + )} diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx index 2f2bcef847b5e..77d1cea588aeb 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/assistant_body/index.tsx @@ -130,6 +130,7 @@ export const AssistantBody: FunctionComponent = ({ setCurrentSystemPromptId={setCurrentSystemPromptId} setIsSettingsModalVisible={setIsSettingsModalVisible} setUserPrompt={setUserPrompt} + connectorId={currentConversation?.apiConfig?.connectorId} /> ) : ( >; } const starterPromptClassName = css` @@ -64,7 +65,7 @@ export const promptGroups = [ }, ]; -export const StarterPrompts: React.FC = ({ setUserPrompt }) => { +export const StarterPrompts: React.FC = ({ connectorId, setUserPrompt }) => { const { assistantAvailability: { isAssistantEnabled }, http, @@ -79,6 +80,7 @@ export const StarterPrompts: React.FC = ({ setUserPrompt }) => { toasts, }, params: { + connector_id: connectorId, prompt_group_id: 'aiAssistant', prompt_ids: getAllPromptIds(promptGroups), }, From 0a8685bbf638ffe1acd41b35b04afd1336a7eb84 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 Jun 2025 08:22:49 -0600 Subject: [PATCH 13/18] add security_labs_tool tests --- .../security_labs/security_labs_tool.test.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.test.ts index 3c49c6563b89a..4836108b65085 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.test.ts @@ -15,7 +15,8 @@ import type { import { newContentReferencesStoreMock } from '@kbn/elastic-assistant-common/impl/content_references/content_references_store/__mocks__/content_references_store.mock'; import type { AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; import { Document } from 'langchain/document'; - +import { getIsKnowledgeBaseInstalled } from '@kbn/elastic-assistant-plugin/server/routes/helpers'; +jest.mock('@kbn/elastic-assistant-plugin/server/routes/helpers'); describe('SecurityLabsTool', () => { const contentReferencesStore = newContentReferencesStoreMock(); const getKnowledgeBaseDocumentEntries = jest.fn(); @@ -100,5 +101,23 @@ In previous publications,`, expect(result).toContain('Citation: {reference(exampleContentReferenceId)}'); }); + it('Responds with The "AI Assistant knowledge base" needs to be installed... when no docs and no kb install', async () => { + getKnowledgeBaseDocumentEntries.mockResolvedValue([]); + getIsKnowledgeBaseInstalled.mockResolvedValue(false); + const tool = SECURITY_LABS_KNOWLEDGE_BASE_TOOL.getTool(defaultArgs) as DynamicStructuredTool; + + const result = await tool.func({ query: 'What is Kibana Security?', product: 'kibana' }); + + expect(result).toContain('The "AI Assistant knowledge base" needs to be installed'); + }); + it('Responds with empty response when no docs and kb is installed', async () => { + getKnowledgeBaseDocumentEntries.mockResolvedValue([]); + getIsKnowledgeBaseInstalled.mockResolvedValue(true); + const tool = SECURITY_LABS_KNOWLEDGE_BASE_TOOL.getTool(defaultArgs) as DynamicStructuredTool; + + const result = await tool.func({ query: 'What is Kibana Security?', product: 'kibana' }); + + expect(result).toEqual('[]'); + }); }); }); From 0355d9943d8b622b3d0cbebf01c1c4070882a998 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 Jun 2025 08:27:05 -0600 Subject: [PATCH 14/18] fix tests --- .../assistant/tools/security_labs/security_labs_tool.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.test.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.test.ts index 4836108b65085..040d755e26117 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.test.ts @@ -103,7 +103,7 @@ In previous publications,`, }); it('Responds with The "AI Assistant knowledge base" needs to be installed... when no docs and no kb install', async () => { getKnowledgeBaseDocumentEntries.mockResolvedValue([]); - getIsKnowledgeBaseInstalled.mockResolvedValue(false); + (getIsKnowledgeBaseInstalled as jest.Mock).mockResolvedValue(false); const tool = SECURITY_LABS_KNOWLEDGE_BASE_TOOL.getTool(defaultArgs) as DynamicStructuredTool; const result = await tool.func({ query: 'What is Kibana Security?', product: 'kibana' }); @@ -112,7 +112,7 @@ In previous publications,`, }); it('Responds with empty response when no docs and kb is installed', async () => { getKnowledgeBaseDocumentEntries.mockResolvedValue([]); - getIsKnowledgeBaseInstalled.mockResolvedValue(true); + (getIsKnowledgeBaseInstalled as jest.Mock).mockResolvedValue(true); const tool = SECURITY_LABS_KNOWLEDGE_BASE_TOOL.getTool(defaultArgs) as DynamicStructuredTool; const result = await tool.func({ query: 'What is Kibana Security?', product: 'kibana' }); From bf0003f3be6711935ead682f79edec550588f5c1 Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:06:19 -0700 Subject: [PATCH 15/18] [Response Ops][Task Manager] Skip API Key Tasks If Security is Disabled (#221732) ## Summary Resolves: https://github.com/elastic/kibana/issues/216810 This PR uses the licensing plugin to check is security is enabled. If not, we skip adding API keys to tasks that pass in a `request` object, but still scheduling them. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../plugins/shared/task_manager/kibana.jsonc | 3 + .../managed_configuration.test.ts | 17 +- .../server/license_subscriber.test.ts | 52 +++++ .../task_manager/server/license_subscriber.ts | 38 ++++ .../shared/task_manager/server/plugin.test.ts | 6 + .../shared/task_manager/server/plugin.ts | 12 +- .../task_manager/server/task_store.test.ts | 207 ++++++++++++++++++ .../shared/task_manager/server/task_store.ts | 24 +- .../plugins/shared/task_manager/tsconfig.json | 1 + 9 files changed, 354 insertions(+), 6 deletions(-) create mode 100644 x-pack/platform/plugins/shared/task_manager/server/license_subscriber.test.ts create mode 100644 x-pack/platform/plugins/shared/task_manager/server/license_subscriber.ts diff --git a/x-pack/platform/plugins/shared/task_manager/kibana.jsonc b/x-pack/platform/plugins/shared/task_manager/kibana.jsonc index 9a68d58df521a..7d95539ae998d 100644 --- a/x-pack/platform/plugins/shared/task_manager/kibana.jsonc +++ b/x-pack/platform/plugins/shared/task_manager/kibana.jsonc @@ -14,6 +14,9 @@ "xpack", "task_manager" ], + "requiredPlugins": [ + "licensing" + ], "optionalPlugins": [ "cloud", "usageCollection", diff --git a/x-pack/platform/plugins/shared/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/platform/plugins/shared/task_manager/server/integration_tests/managed_configuration.test.ts index 721ee66c11bb0..8a514798a42a6 100644 --- a/x-pack/platform/plugins/shared/task_manager/server/integration_tests/managed_configuration.test.ts +++ b/x-pack/platform/plugins/shared/task_manager/server/integration_tests/managed_configuration.test.ts @@ -17,6 +17,7 @@ import { TaskManagerPlugin } from '../plugin'; import { coreMock } from '@kbn/core/server/mocks'; import type { TaskManagerConfig } from '../config'; import { BulkUpdateError } from '../lib/bulk_update_error'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; const mockTaskTypeRunFn = jest.fn(); const mockCreateTaskRunner = jest.fn(); @@ -135,7 +136,9 @@ describe('managed configuration', () => { esStart.client.asInternalUser as unknown as Client ); coreStart.savedObjects.createInternalRepository.mockReturnValue(savedObjectsClient); - taskManagerStart = await taskManager.start(coreStart, {}); + taskManagerStart = await taskManager.start(coreStart, { + licensing: licensingMock.createStart(), + }); // force rxjs timers to fire when they are scheduled for setTimeout(0) as the // sinon fake timers cause them to stall. We need to do this a few times for the @@ -233,7 +236,9 @@ describe('managed configuration', () => { esStart.client.asInternalUser as unknown as Client ); coreStart.savedObjects.createInternalRepository.mockReturnValue(savedObjectsClient); - taskManagerStart = taskManager.start(coreStart, {}); + taskManagerStart = taskManager.start(coreStart, { + licensing: licensingMock.createStart(), + }); // force rxjs timers to fire when they are scheduled for setTimeout(0) as the // sinon fake timers cause them to stall. We need to do this a few times for the @@ -335,7 +340,9 @@ describe('managed configuration', () => { esStart.client.asInternalUser as unknown as Client ); coreStart.savedObjects.createInternalRepository.mockReturnValue(savedObjectsClient); - taskManagerStart = await taskManager.start(coreStart, {}); + taskManagerStart = await taskManager.start(coreStart, { + licensing: licensingMock.createStart(), + }); // force rxjs timers to fire when they are scheduled for setTimeout(0) as the // sinon fake timers cause them to stall. We need to do this a few times for the @@ -420,7 +427,9 @@ describe('managed configuration', () => { esStart.client.asInternalUser as unknown as Client ); coreStart.savedObjects.createInternalRepository.mockReturnValue(savedObjectsClient); - taskManagerStart = await taskManager.start(coreStart, {}); + taskManagerStart = await taskManager.start(coreStart, { + licensing: licensingMock.createStart(), + }); // force rxjs timers to fire when they are scheduled for setTimeout(0) as the // sinon fake timers cause them to stall diff --git a/x-pack/platform/plugins/shared/task_manager/server/license_subscriber.test.ts b/x-pack/platform/plugins/shared/task_manager/server/license_subscriber.test.ts new file mode 100644 index 0000000000000..d81799d30decd --- /dev/null +++ b/x-pack/platform/plugins/shared/task_manager/server/license_subscriber.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { LicenseSubscriber } from './license_subscriber'; +import { Subject } from 'rxjs'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import type { ILicense } from '@kbn/licensing-plugin/server'; + +describe('LicenseSubscriber', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getIsSecurityEnabled', () => { + test('should return true if security is enabled', () => { + const license: Subject = new Subject(); + const licenseSubscriber = new LicenseSubscriber(license); + + const basicLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'basic' }, + features: { security: { isEnabled: true, isAvailable: true } }, + }); + + license.next(basicLicense); + expect(licenseSubscriber.getIsSecurityEnabled()).toEqual(true); + }); + + test('should return false if security doesnt exist', () => { + const license: Subject = new Subject(); + const licenseSubscriber = new LicenseSubscriber(license); + + expect(licenseSubscriber.getIsSecurityEnabled()).toEqual(false); + }); + + test('should return false if security is disabled', () => { + const license: Subject = new Subject(); + const licenseSubscriber = new LicenseSubscriber(license); + + const basicLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'basic' }, + features: { security: { isEnabled: false, isAvailable: true } }, + }); + + license.next(basicLicense); + expect(licenseSubscriber.getIsSecurityEnabled()).toEqual(false); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/task_manager/server/license_subscriber.ts b/x-pack/platform/plugins/shared/task_manager/server/license_subscriber.ts new file mode 100644 index 0000000000000..6e3c750fbaf32 --- /dev/null +++ b/x-pack/platform/plugins/shared/task_manager/server/license_subscriber.ts @@ -0,0 +1,38 @@ +/* + * 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 { ILicense } from '@kbn/licensing-plugin/server'; +import type { Observable, Subscription } from 'rxjs'; + +export class LicenseSubscriber { + private subscription: Subscription; + + private licenseState?: ILicense; + + constructor(license: Observable) { + this.getIsSecurityEnabled = this.getIsSecurityEnabled.bind(this); + this.updateState = this.updateState.bind(this); + + this.subscription = license.subscribe(this.updateState); + } + + private updateState(license: ILicense | undefined) { + this.licenseState = license; + } + + public getIsSecurityEnabled() { + if (!this.licenseState || !this.licenseState.isAvailable) { + return false; + } + + return this.licenseState.getFeature('security').isEnabled; + } + + public cleanup() { + this.subscription.unsubscribe(); + } +} diff --git a/x-pack/platform/plugins/shared/task_manager/server/plugin.test.ts b/x-pack/platform/plugins/shared/task_manager/server/plugin.test.ts index 65ee100ab3666..4e9735a44c661 100644 --- a/x-pack/platform/plugins/shared/task_manager/server/plugin.test.ts +++ b/x-pack/platform/plugins/shared/task_manager/server/plugin.test.ts @@ -14,6 +14,7 @@ import { cloudMock } from '@kbn/cloud-plugin/public/mocks'; import { taskPollingLifecycleMock } from './polling_lifecycle.mock'; import { TaskPollingLifecycle } from './polling_lifecycle'; import type { TaskPollingLifecycle as TaskPollingLifecycleClass } from './polling_lifecycle'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; let mockTaskPollingLifecycle = taskPollingLifecycleMock.create({}); jest.mock('./polling_lifecycle', () => { @@ -140,6 +141,7 @@ describe('TaskManagerPlugin', () => { taskManagerPlugin.setup(coreMock.createSetup(), { usageCollection: undefined }); taskManagerPlugin.start(coreStart, { cloud: cloudMock.createStart(), + licensing: licensingMock.createStart(), }); expect(TaskPollingLifecycle as jest.Mock).toHaveBeenCalledTimes(1); @@ -154,6 +156,7 @@ describe('TaskManagerPlugin', () => { taskManagerPlugin.setup(coreMock.createSetup(), { usageCollection: undefined }); taskManagerPlugin.start(coreStart, { cloud: cloudMock.createStart(), + licensing: licensingMock.createStart(), }); expect(TaskPollingLifecycle as jest.Mock).not.toHaveBeenCalled(); @@ -170,6 +173,7 @@ describe('TaskManagerPlugin', () => { taskManagerPlugin.setup(coreMock.createSetup(), { usageCollection: undefined }); taskManagerPlugin.start(coreStart, { cloud: cloudMock.createStart(), + licensing: licensingMock.createStart(), }); expect(TaskPollingLifecycle as jest.Mock).toHaveBeenCalledTimes(1); @@ -188,6 +192,7 @@ describe('TaskManagerPlugin', () => { taskManagerPlugin.setup(coreMock.createSetup(), { usageCollection: undefined }); taskManagerPlugin.start(coreStart, { cloud: cloudMock.createStart(), + licensing: licensingMock.createStart(), }); await taskManagerPlugin.stop(); @@ -204,6 +209,7 @@ describe('TaskManagerPlugin', () => { taskManagerPlugin.setup(coreMock.createSetup(), { usageCollection: undefined }); taskManagerPlugin.start(coreStart, { cloud: cloudMock.createStart(), + licensing: licensingMock.createStart(), }); discoveryIsStarted.mockReturnValueOnce(true); diff --git a/x-pack/platform/plugins/shared/task_manager/server/plugin.ts b/x-pack/platform/plugins/shared/task_manager/server/plugin.ts index 87c33dac4b4b5..c4f55891f8231 100644 --- a/x-pack/platform/plugins/shared/task_manager/server/plugin.ts +++ b/x-pack/platform/plugins/shared/task_manager/server/plugin.ts @@ -23,6 +23,8 @@ import type { } from '@kbn/core/server'; import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server'; import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-shared'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/server'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { registerDeleteInactiveNodesTaskDefinition, scheduleDeleteInactiveNodesTaskDefinition, @@ -58,6 +60,7 @@ import { scheduleMarkRemovedTasksAsUnrecognizedDefinition, } from './removed_tasks/mark_removed_tasks_as_unrecognized'; import { getElasticsearchAndSOAvailability } from './lib/get_es_and_so_availability'; +import { LicenseSubscriber } from './license_subscriber'; export interface TaskManagerSetupContract { /** @@ -92,6 +95,7 @@ export type TaskManagerStartContract = Pick< }; export interface TaskManagerPluginsStart { + licensing: LicensingPluginStart; cloud?: CloudStart; usageCollection?: UsageCollectionStart; } @@ -132,6 +136,7 @@ export class TaskManagerPlugin private heapSizeLimit: number = 0; private numOfKibanaInstances$: Subject = new BehaviorSubject(1); private canEncryptSavedObjects: boolean; + private licenseSubscriber?: PublicMethodsOf; constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; @@ -286,8 +291,10 @@ export class TaskManagerPlugin public start( { http, savedObjects, elasticsearch, executionContext, security }: CoreStart, - { cloud }: TaskManagerPluginsStart + { cloud, licensing }: TaskManagerPluginsStart ): TaskManagerStartContract { + this.licenseSubscriber = new LicenseSubscriber(licensing.license$); + const savedObjectsRepository = savedObjects.createInternalRepository([ TASK_SO_NAME, BACKGROUND_TASK_NODE_SO_NAME, @@ -320,6 +327,7 @@ export class TaskManagerPlugin requestTimeouts: this.config.request_timeouts, security, canEncryptSavedObjects: this.canEncryptSavedObjects, + getIsSecurityEnabled: this.licenseSubscriber?.getIsSecurityEnabled, }); const isServerless = this.initContext.env.packageInfo.buildFlavor === 'serverless'; @@ -431,6 +439,8 @@ export class TaskManagerPlugin } public async stop() { + this.licenseSubscriber?.cleanup(); + // Stop polling for tasks if (this.taskPollingLifecycle) { this.taskPollingLifecycle.stop(); diff --git a/x-pack/platform/plugins/shared/task_manager/server/task_store.test.ts b/x-pack/platform/plugins/shared/task_manager/server/task_store.test.ts index 367d79bdb7b9d..f779fc4a18266 100644 --- a/x-pack/platform/plugins/shared/task_manager/server/task_store.test.ts +++ b/x-pack/platform/plugins/shared/task_manager/server/task_store.test.ts @@ -136,6 +136,7 @@ describe('TaskStore', () => { savedObjectsService: coreStart.savedObjects, security: coreStart.security, canEncryptSavedObjects: true, + getIsSecurityEnabled: () => true, }); store.registerEncryptedSavedObjectsClient(esoClient); @@ -246,6 +247,70 @@ describe('TaskStore', () => { expect(attributes.state).toEqual('{}'); }); + test('schedule a task without API key if request is provided but security disabled', async () => { + store = new TaskStore({ + logger: mockLogger(), + index: 'tasky', + taskManagerId: '', + serializer, + esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, + definitions: taskDefinitions, + savedObjectsRepository: savedObjectsClient, + adHocTaskCounter, + allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, + savedObjectsService: coreStart.savedObjects, + security: coreStart.security, + canEncryptSavedObjects: true, + getIsSecurityEnabled: () => false, + }); + + store.registerEncryptedSavedObjectsClient(esoClient); + + const task = { + id: 'id', + params: { hello: 'world' }, + state: { foo: 'bar' }, + taskType: 'report', + traceparent: 'apmTraceparent', + }; + + const request = httpServerMock.createKibanaRequest(); + + savedObjectsClient.create.mockImplementation(async (type: string, attributes: unknown) => ({ + id: 'testid', + type, + attributes, + references: [], + version: '123', + })); + + await store.schedule(task as TaskInstance, { request }); + + expect(savedObjectsClient.create).toHaveBeenCalledWith( + 'task', + { + attempts: 0, + params: '{"hello":"world"}', + retryAt: null, + runAt: '2019-02-12T21:01:22.479Z', + scheduledAt: '2019-02-12T21:01:22.479Z', + startedAt: null, + state: '{"foo":"bar"}', + status: 'idle', + taskType: 'report', + traceparent: 'apmTraceparent', + partition: 225, + }, + { + id: 'id', + refresh: false, + } + ); + }); + test('schedule a task with API key if request is provided', async () => { const task = { id: 'id', @@ -347,6 +412,7 @@ describe('TaskStore', () => { savedObjectsService: coreStart.savedObjects, security: coreStart.security, canEncryptSavedObjects: false, + getIsSecurityEnabled: () => true, }); const task = { @@ -473,6 +539,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); }); @@ -603,6 +670,7 @@ describe('TaskStore', () => { savedObjectsService: coreStart.savedObjects, security: coreStart.security, canEncryptSavedObjects: true, + getIsSecurityEnabled: () => true, }); esoClient.createPointInTimeFinderDecryptedAsInternalUser = jest.fn().mockResolvedValue({ @@ -788,6 +856,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); }); @@ -914,6 +983,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); }); @@ -1112,6 +1182,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); }); @@ -1339,6 +1410,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); }); @@ -1889,6 +1961,7 @@ describe('TaskStore', () => { savedObjectsService: coreStart.savedObjects, security: coreStart.security, canEncryptSavedObjects: true, + getIsSecurityEnabled: () => true, }); esoClient.createPointInTimeFinderDecryptedAsInternalUser = jest.fn().mockResolvedValue({ @@ -2009,6 +2082,7 @@ describe('TaskStore', () => { savedObjectsService: coreStart.savedObjects, security: coreStart.security, canEncryptSavedObjects: true, + getIsSecurityEnabled: () => true, }); esoClient.createPointInTimeFinderDecryptedAsInternalUser = jest.fn().mockResolvedValue({ @@ -2079,6 +2153,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); }); @@ -2144,6 +2219,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); }); @@ -2244,6 +2320,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); expect(await store.getLifecycle(task.id)).toEqual(status); @@ -2271,6 +2348,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); expect(await store.getLifecycle(randomId())).toEqual(TaskLifecycleResult.NotFound); @@ -2296,6 +2374,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); return expect(store.getLifecycle(randomId())).rejects.toThrow('Bad Request'); @@ -2323,6 +2402,7 @@ describe('TaskStore', () => { savedObjectsService: coreStart.savedObjects, security: coreStart.security, canEncryptSavedObjects: true, + getIsSecurityEnabled: () => true, }); store.registerEncryptedSavedObjectsClient(esoClient); @@ -2590,6 +2670,7 @@ describe('TaskStore', () => { savedObjectsService: coreStart.savedObjects, security: coreStart.security, canEncryptSavedObjects: false, + getIsSecurityEnabled: () => true, }); const task1 = { @@ -2606,6 +2687,127 @@ describe('TaskStore', () => { ); }); + test('bulk schedule tasks without API key if request is provided but security disabled', async () => { + store = new TaskStore({ + logger: mockLogger(), + index: 'tasky', + taskManagerId: '', + serializer, + esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, + definitions: taskDefinitions, + savedObjectsRepository: savedObjectsClient, + adHocTaskCounter, + allowReadingInvalidState: false, + requestTimeouts: { + update_by_query: 1000, + }, + savedObjectsService: coreStart.savedObjects, + security: coreStart.security, + canEncryptSavedObjects: true, + getIsSecurityEnabled: () => false, + }); + + store.registerEncryptedSavedObjectsClient(esoClient); + + const task1 = { + id: 'task1', + params: { hello: 'world' }, + state: { foo: 'bar' }, + taskType: 'report', + }; + + const task2 = { + id: 'task2', + params: { hello: 'world' }, + state: { foo: 'bar' }, + taskType: 'report', + }; + + const request = httpServerMock.createKibanaRequest(); + + savedObjectsClient.bulkCreate.mockImplementation(async () => ({ + saved_objects: [ + { + id: 'task1', + type: 'test', + attributes: { + attempts: 0, + params: '{"hello":"world"}', + retryAt: null, + runAt: '2019-02-12T21:01:22.479Z', + scheduledAt: '2019-02-12T21:01:22.479Z', + startedAt: null, + state: '{"foo":"bar"}', + stateVersion: 1, + status: 'idle', + taskType: 'report', + traceparent: 'apmTraceparent', + partition: 225, + }, + references: [], + version: '123', + }, + { + id: 'task2', + type: 'test', + attributes: { + attempts: 0, + params: '{"hello":"world"}', + retryAt: null, + runAt: '2019-02-12T21:01:22.479Z', + scheduledAt: '2019-02-12T21:01:22.479Z', + startedAt: null, + state: '{"foo":"bar"}', + stateVersion: 1, + status: 'idle', + taskType: 'report', + traceparent: 'apmTraceparent', + partition: 225, + }, + references: [], + version: '123', + }, + ], + })); + + const result = await store.bulkSchedule([task1, task2], { request }); + + expect(result).toEqual([ + { + id: 'task1', + attempts: 0, + params: { hello: 'world' }, + retryAt: null, + runAt: mockedDate, + scheduledAt: mockedDate, + startedAt: null, + state: { foo: 'bar' }, + stateVersion: 1, + status: 'idle', + taskType: 'report', + traceparent: 'apmTraceparent', + partition: 225, + version: '123', + }, + { + id: 'task2', + attempts: 0, + params: { hello: 'world' }, + retryAt: null, + runAt: mockedDate, + scheduledAt: mockedDate, + startedAt: null, + state: { foo: 'bar' }, + stateVersion: 1, + status: 'idle', + taskType: 'report', + traceparent: 'apmTraceparent', + partition: 225, + version: '123', + }, + ]); + }); + test('errors if API key could not be created', async () => { const task = { id: 'id', @@ -2756,6 +2958,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); savedObjectsClient.create.mockImplementation(async (type: string, attributes: unknown) => ({ @@ -2806,6 +3009,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); savedObjectsClient.create.mockImplementation(async (type: string, attributes: unknown) => ({ @@ -2857,6 +3061,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); }); test('should pass requestTimeout', async () => { @@ -2896,6 +3101,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); }); @@ -3016,6 +3222,7 @@ describe('TaskStore', () => { }, savedObjectsService: coreStart.savedObjects, security: coreStart.security, + getIsSecurityEnabled: () => true, }); }); diff --git a/x-pack/platform/plugins/shared/task_manager/server/task_store.ts b/x-pack/platform/plugins/shared/task_manager/server/task_store.ts index f1d37b3387ac2..df045724a9a4b 100644 --- a/x-pack/platform/plugins/shared/task_manager/server/task_store.ts +++ b/x-pack/platform/plugins/shared/task_manager/server/task_store.ts @@ -81,6 +81,7 @@ export interface StoreOpts { security: SecurityServiceStart; canEncryptSavedObjects?: boolean; esoClient?: EncryptedSavedObjectsClient; + getIsSecurityEnabled: () => boolean; } export interface SearchOpts { @@ -150,6 +151,7 @@ export class TaskStore { private requestTimeouts: RequestTimeoutsConfig; private security: SecurityServiceStart; private canEncryptSavedObjects?: boolean; + private getIsSecurityEnabled: () => boolean; private logger: Logger; /** @@ -184,6 +186,7 @@ export class TaskStore { this.requestTimeouts = opts.requestTimeouts; this.security = opts.security; this.canEncryptSavedObjects = opts.canEncryptSavedObjects; + this.getIsSecurityEnabled = opts.getIsSecurityEnabled; this.logger = opts.logger; } @@ -207,7 +210,7 @@ export class TaskStore { } private getSoClientForCreate(options: ApiKeyOptions) { - if (options.request) { + if (options.request && this.getIsSecurityEnabled()) { return this.savedObjectsService.getScopedClient(options.request, { includedHiddenTypes: [TASK_SO_NAME], excludedExtensions: [SECURITY_EXTENSION_ID, SPACES_EXTENSION_ID], @@ -217,6 +220,10 @@ export class TaskStore { } private async getApiKeyFromRequest(taskInstances: TaskInstance[], request?: KibanaRequest) { + if (!this.getIsSecurityEnabled()) { + return null; + } + if (!request) { return null; } @@ -349,6 +356,13 @@ export class TaskStore { } const result = savedObjectToConcreteTaskInstance(savedObject); + + if (options?.request && !this.getIsSecurityEnabled()) { + this.logger.info( + `Trying to schedule task ${result.id} with user scope but security is disabled. Task will run without user scope.` + ); + } + return this.taskValidator.getValidatedTaskInstanceFromReading(result); } @@ -420,6 +434,14 @@ export class TaskStore { throw e; } + if (options?.request && !this.getIsSecurityEnabled()) { + this.logger.info( + `Trying to bulk schedule tasks ${JSON.stringify( + savedObjects.saved_objects.map((so) => so.id) + )} with user scope but security is disabled. Tasks will run without user scope.` + ); + } + return savedObjects.saved_objects.map((so) => { const taskInstance = savedObjectToConcreteTaskInstance(so); return this.taskValidator.getValidatedTaskInstanceFromReading(taskInstance); diff --git a/x-pack/platform/plugins/shared/task_manager/tsconfig.json b/x-pack/platform/plugins/shared/task_manager/tsconfig.json index ac97e02f22261..3297f338a8387 100644 --- a/x-pack/platform/plugins/shared/task_manager/tsconfig.json +++ b/x-pack/platform/plugins/shared/task_manager/tsconfig.json @@ -34,6 +34,7 @@ "@kbn/encrypted-saved-objects-shared", "@kbn/core-saved-objects-api-server-mocks", "@kbn/core-http-server-utils", + "@kbn/licensing-plugin", "@kbn/rrule", "@kbn/logging-mocks", "@kbn/spaces-utils", From 2e384fb0a56e84a2ea60c113fa009642e9fb8925 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 Jun 2025 20:32:51 -0600 Subject: [PATCH 16/18] [Security Assistant] Context prompts (#224956) --- .../kbn-elastic-assistant-common/constants.ts | 6 + .../impl/schemas/common_attributes.gen.ts | 2 +- .../schemas/common_attributes.schema.yaml | 2 +- .../impl/schemas/index.ts | 2 + .../common_attributes.gen.ts | 29 +++ .../common_attributes.schema.yaml | 26 +++ .../find_prompts_route.gen.ts | 44 ++++ .../find_prompts_route.schema.yaml | 63 +++++ .../use_find_prompts.test.tsx | 97 ++++++++ .../security_ai_prompts/use_find_prompts.ts | 107 +++++++++ .../shared/kbn-elastic-assistant/index.ts | 2 + .../translations/translations/fr-FR.json | 12 +- .../translations/translations/ja-JP.json | 12 +- .../translations/translations/zh-CN.json | 12 +- .../actions/chat/index.test.tsx | 220 +++++++++++++++++- .../data_quality_panel/actions/chat/index.tsx | 35 ++- .../utils/get_formatted_check_time.test.ts | 8 + .../utils/get_formatted_check_time.ts | 4 +- .../impl/data_quality_panel/translations.ts | 4 +- .../src/get_prompt.test.ts | 27 +++ .../security-ai-prompts/src/get_prompt.ts | 15 +- .../packages/security-ai-prompts/src/types.ts | 4 +- .../server/__mocks__/request.ts | 8 + .../server/lib/prompt/local_prompt_object.ts | 28 +++ .../server/lib/prompt/prompts.ts | 37 +++ .../server/routes/register_routes.ts | 4 + .../security_ai_prompts/find_prompts.test.ts | 106 +++++++++ .../security_ai_prompts/find_prompts.ts | 92 ++++++++ .../content/prompt_contexts/index.tsx | 18 +- .../use_find_prompt_contexts.test.tsx | 108 +++++++++ .../use_find_prompt_contexts.tsx | 52 +++++ .../content/prompts/user/translations.ts | 39 ++-- .../public/assistant/provider.tsx | 27 ++- .../detection_engine/common/translations.ts | 11 +- .../right/hooks/use_assistant.test.tsx | 76 ++++-- .../right/hooks/use_assistant.ts | 16 +- .../security_solution/public/plugin.tsx | 5 - .../e2e/ai_assistant/conversations.cy.ts | 12 +- .../cypress/e2e/ai_assistant/prompts.cy.ts | 5 - 39 files changed, 1262 insertions(+), 115 deletions(-) create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/common_attributes.gen.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/common_attributes.schema.yaml create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/find_prompts_route.gen.ts create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/find_prompts_route.schema.yaml create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.test.tsx create mode 100644 x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.ts create mode 100644 x-pack/solutions/security/plugins/elastic_assistant/server/routes/security_ai_prompts/find_prompts.test.ts create mode 100644 x-pack/solutions/security/plugins/elastic_assistant/server/routes/security_ai_prompts/find_prompts.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompt_contexts/use_find_prompt_contexts.test.tsx create mode 100644 x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompt_contexts/use_find_prompt_contexts.tsx diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/constants.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/constants.ts index f1e4153fba887..55ef349a897da 100755 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/constants.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/constants.ts @@ -67,6 +67,12 @@ export const ELASTIC_AI_ASSISTANT_ALERT_SUMMARY_URL_BULK_ACTION = export const ELASTIC_AI_ASSISTANT_ALERT_SUMMARY_URL_FIND = `${ELASTIC_AI_ASSISTANT_ALERT_SUMMARY_URL}/_find` as const; +// Security AI Prompts (prompt integration) +export const ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL = + `${ELASTIC_AI_ASSISTANT_URL}/security_ai_prompts` as const; +export const ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND = + `${ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL}/_find` as const; + // Defend insights export const DEFEND_INSIGHTS_ID = 'defend-insights'; export const DEFEND_INSIGHTS = `${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/defend_insights`; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/common_attributes.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/common_attributes.gen.ts index fa46c6ff0be38..d62e93e6a59b1 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/common_attributes.gen.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/common_attributes.gen.ts @@ -102,7 +102,7 @@ export const BulkActionBase = z.object({ }); /** - * User screen context. + * IDs for a specific prompt within a group of prompts. */ export type PromptIds = z.infer; export const PromptIds = z.object({ diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/common_attributes.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/common_attributes.schema.yaml index 036584653f770..b98ef548c709f 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/common_attributes.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/common_attributes.schema.yaml @@ -101,7 +101,7 @@ components: - "5678" PromptIds: - description: User screen context. + description: IDs for a specific prompt within a group of prompts. type: object required: - promptId diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/index.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/index.ts index 242b6c8e70749..704821b9e1dc6 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/index.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/index.ts @@ -75,4 +75,6 @@ export * from './prompts/find_prompts_route.gen'; export * from './alert_summary/bulk_crud_alert_summary_route.gen'; export * from './alert_summary/find_alert_summary_route.gen'; +export * from './security_ai_prompts/find_prompts_route.gen'; + export { PromptResponse, PromptTypeEnum } from './prompts/bulk_crud_prompts_route.gen'; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/common_attributes.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/common_attributes.gen.ts new file mode 100644 index 0000000000000..aa9197f2260e1 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/common_attributes.gen.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Common Security AI Prompts Attributes + * version: not applicable + */ + +import { z } from '@kbn/zod'; + +export type PromptItem = z.infer; +export const PromptItem = z.object({ + promptId: z.string(), + prompt: z.string(), +}); + +/** + * Prompt array by prompt group id and prompt id. + */ +export type PromptItemArray = z.infer; +export const PromptItemArray = z.array(PromptItem); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/common_attributes.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/common_attributes.schema.yaml new file mode 100644 index 0000000000000..f6d582512ff3c --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/common_attributes.schema.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.0 +info: + title: Common Security AI Prompts Attributes + version: 'not applicable' +paths: {} +components: + x-codegen-enabled: true + schemas: + PromptItem: + type: object + properties: + promptId: + type: string + example: systemPrompt + prompt: + type: string + example: This is the prompt + required: + - promptId + - prompt + + PromptItemArray: + type: array + description: Prompt array by prompt group id and prompt id. + items: + $ref: '#/components/schemas/PromptItem' diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/find_prompts_route.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/find_prompts_route.gen.ts new file mode 100644 index 0000000000000..542beef9bb08c --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/find_prompts_route.gen.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Get Security AI Prompt endpoint + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; +import { ArrayFromString } from '@kbn/zod-helpers'; + +import { PromptItemArray } from './common_attributes.gen'; + +export type FindSecurityAIPromptsRequestQuery = z.infer; +export const FindSecurityAIPromptsRequestQuery = z.object({ + /** + * Connector id used for prompt lookup + */ + connector_id: z.string().optional(), + /** + * The unique identifier for the prompt group + */ + prompt_group_id: z.string(), + /** + * Comma-separated list of prompt IDs to retrieve + */ + prompt_ids: ArrayFromString(z.string()), +}); +export type FindSecurityAIPromptsRequestQueryInput = z.input< + typeof FindSecurityAIPromptsRequestQuery +>; + +export type FindSecurityAIPromptsResponse = z.infer; +export const FindSecurityAIPromptsResponse = z.object({ + prompts: PromptItemArray, +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/find_prompts_route.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/find_prompts_route.schema.yaml new file mode 100644 index 0000000000000..623ec6da82a72 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/security_ai_prompts/find_prompts_route.schema.yaml @@ -0,0 +1,63 @@ +openapi: 3.0.0 +info: + title: Get Security AI Prompt endpoint + version: '2023-10-31' +paths: + /api/security_ai_assistant/security_ai_prompts/_find: + get: + x-codegen-enabled: true + x-internal: true + x-labels: [ess, serverless] + operationId: FindSecurityAIPrompts + description: Gets Security AI prompts + summary: Gets Security AI prompts + tags: + - Security AI Prompts API + parameters: + - name: connector_id + in: query + description: Connector id used for prompt lookup + required: false + schema: + type: string + - name: prompt_group_id + in: query + description: The unique identifier for the prompt group + required: true + schema: + type: string + - name: prompt_ids + in: query + description: Comma-separated list of prompt IDs to retrieve + required: true + style: form + explode: true + schema: + type: array + items: + type: string + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + prompts: + $ref: './common_attributes.schema.yaml#/components/schemas/PromptItemArray' + required: + - prompts + '400': + description: Generic Error + content: + application/json: + schema: + type: object + properties: + status_code: + type: number + error: + type: string + message: + type: string diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.test.tsx new file mode 100644 index 0000000000000..ac6f064d45578 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.test.tsx @@ -0,0 +1,97 @@ +/* + * 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 { waitFor, renderHook } from '@testing-library/react'; +import { useFindPrompts, UseFindPromptsParams } from './use_find_prompts'; +import { + API_VERSIONS, + ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND, +} from '@kbn/elastic-assistant-common'; +import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { IToasts } from '@kbn/core-notifications-browser'; + +const mockHttpFetch = jest.fn(); +const mockToasts = { + addSuccess: jest.fn(), + addError: jest.fn(), +} as unknown as IToasts; + +describe('useFindPrompts', () => { + const params: UseFindPromptsParams = { + context: { + isAssistantEnabled: true, + httpFetch: mockHttpFetch, + toasts: mockToasts, + }, + params: { + connector_id: 'connector-1', + prompt_ids: ['prompt-1'], + prompt_group_id: 'group-1', + }, + signal: undefined, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns initial and placeholder data when loading', async () => { + mockHttpFetch.mockResolvedValueOnce({ prompts: [{ id: 'prompt-1', name: 'Prompt 1' }] }); + const { result } = renderHook(() => useFindPrompts(params), { + wrapper: TestProviders, + }); + expect(result.current.data).toEqual({ prompts: [] }); + await waitFor(() => result.current.isSuccess); + + await waitFor(() => + expect(result.current.data).toEqual({ prompts: [{ id: 'prompt-1', name: 'Prompt 1' }] }) + ); + }); + + it('disables the query if isAssistantEnabled is false', async () => { + const disabledParams = { ...params, context: { ...params.context, isAssistantEnabled: false } }; + const { result } = renderHook(() => useFindPrompts(disabledParams), { wrapper: TestProviders }); + expect(result.current.isLoading).toBe(false); + expect(result.current.data).toEqual({ prompts: [] }); + expect(mockHttpFetch).not.toHaveBeenCalled(); + }); + + it('calls httpFetch with correct arguments', async () => { + mockHttpFetch.mockResolvedValueOnce({ prompts: [{ id: 'prompt-1', name: 'Prompt 1' }] }); + const { result } = renderHook(() => useFindPrompts(params), { + wrapper: TestProviders, + }); + await waitFor(() => result.current.isSuccess); + expect(mockHttpFetch).toHaveBeenCalledWith( + ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND, + expect.objectContaining({ + method: 'GET', + version: API_VERSIONS.public.v1, + query: { + connector_id: 'connector-1', + prompt_ids: ['prompt-1'], + prompt_group_id: 'group-1', + }, + }) + ); + }); + + it('shows a toast on error', async () => { + const error = { body: { message: 'Something went wrong' } }; + mockHttpFetch.mockRejectedValueOnce(error); + const { result } = renderHook(() => useFindPrompts(params), { + wrapper: TestProviders, + }); + await waitFor(() => result.current.isError); + expect(mockToasts.addError).toHaveBeenCalledWith( + expect.any(Error), + expect.objectContaining({ + title: expect.any(String), + }) + ); + }); +}); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.ts new file mode 100644 index 0000000000000..01c9cbf773592 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/api/security_ai_prompts/use_find_prompts.ts @@ -0,0 +1,107 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { HttpHandler, IToasts } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { + API_VERSIONS, + ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND, + FindSecurityAIPromptsRequestQuery, + FindSecurityAIPromptsResponse, +} from '@kbn/elastic-assistant-common'; + +export interface UseFindPromptsParams { + context: { + isAssistantEnabled: boolean; + httpFetch: HttpHandler; + toasts: IToasts; + }; + signal?: AbortSignal | undefined; + params: FindSecurityAIPromptsRequestQuery; +} + +/** + * API call for fetching prompts for current spaceId + * + * @param {Object} options - The options object. + * @param {string} options.consumer - prompt consumer + * @param {AbortSignal} [options.signal] - AbortSignal + * + * @returns {useQuery} hook for getting the status of the prompts + */ + +export const useFindPrompts = (payload: UseFindPromptsParams) => { + const { isAssistantEnabled, httpFetch, toasts } = payload.context; + + const QUERY = { + connector_id: payload.params.connector_id, + prompt_ids: payload.params.prompt_ids, + prompt_group_id: payload.params.prompt_group_id, + }; + + const CACHING_KEYS = [ + ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND, + QUERY.connector_id, + QUERY.prompt_ids, + QUERY.prompt_group_id, + API_VERSIONS.public.v1, + ]; + + return useQuery( + CACHING_KEYS, + async () => + getPrompts({ + httpFetch, + signal: payload.signal, + toasts, + query: QUERY, + }), + { + initialData: { + prompts: [], + }, + placeholderData: { + prompts: [], + }, + keepPreviousData: true, + refetchOnWindowFocus: false, + enabled: isAssistantEnabled, + } + ); +}; + +const getPrompts = async ({ + httpFetch, + signal, + toasts, + query, +}: { + httpFetch: HttpHandler; + toasts: IToasts; + signal?: AbortSignal | undefined; + query: FindSecurityAIPromptsRequestQuery; +}) => { + try { + return await httpFetch( + ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND, + { + method: 'GET', + version: API_VERSIONS.public.v1, + signal, + query, + } + ); + } catch (error) { + toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, { + title: i18n.translate('xpack.elasticAssistant.securityAiPrompts.getPromptsError', { + defaultMessage: 'Error fetching Security AI Prompts', + }), + }); + throw error; + } +}; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/index.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/index.ts index bb37119cf22e7..569ed723f2995 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/index.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/index.ts @@ -184,6 +184,8 @@ export { getCombinedMessage } from './impl/assistant/prompt/helpers'; export { useChatComplete } from './impl/assistant/api/chat_complete/use_chat_complete'; export { useFetchAnonymizationFields } from './impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields'; +export { useFindPrompts } from './impl/assistant/api/security_ai_prompts/use_find_prompts'; + export interface UseAssistantAvailability { // True when searchAiLake configurations is available hasSearchAILakeConfigurations: boolean; diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index de31cdc510f75..afcf3d95bdf05 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -7529,7 +7529,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.dataQualityDashboardConversationId": "Tableau de bord de Qualité des données", "securitySolutionPackages.ecsDataQualityDashboard.dataQualityPromptContextPill": "Qualité des données ({indexName})", "securitySolutionPackages.ecsDataQualityDashboard.dataQualityPromptContextPillTooltip": "Ajoutez ce rapport de Qualité des données comme contexte", - "securitySolutionPackages.ecsDataQualityDashboard.dataQualitySuggestedUserPrompt": "Expliquez les résultats ci-dessus et donnez des options pour résoudre les incompatibilités.", "securitySolutionPackages.ecsDataQualityDashboard.defaultPanelTitle": "Vérifier les mappings d'index", "securitySolutionPackages.ecsDataQualityDashboard.detectionEngineRulesWontWorkMessage": "❌ Les règles de moteur de détection référençant ces champs ne leur correspondront peut-être pas correctement", "securitySolutionPackages.ecsDataQualityDashboard.docs": "Documents", @@ -16363,6 +16362,13 @@ "xpack.elasticAssistant.prompts.getPromptsError": "Erreur lors de la récupération des invites", "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "L'assistant d'IA d'Elastic n'est accessible qu'aux entreprises. Veuillez mettre votre licence à niveau pour bénéficier de cette fonctionnalité.", "xpack.elasticAssistantPlugin.assistant.apiErrorTitle": "Une erreur s’est produite lors de l’envoi de votre message.", + "xpack.elasticAssistantPlugin.assistant.contentReferences.knowledgeBaseEntryReference.label": "Entrée de la base de connaissances", + "xpack.elasticAssistantPlugin.assistant.contentReferences.securityAlertReference.label": "Afficher l'alerte", + "xpack.elasticAssistantPlugin.assistant.contentReferences.securityAlertsPageReference.label": "Afficher les alertes", + "xpack.elasticAssistantPlugin.assistant.getComments.assistant": "Assistant", + "xpack.elasticAssistantPlugin.assistant.getComments.at": "à : {timestamp}", + "xpack.elasticAssistantPlugin.assistant.getComments.system": "Système", + "xpack.elasticAssistantPlugin.assistant.getComments.you": "Vous", "xpack.elasticAssistantPlugin.assistant.quickPrompts.esqlQueryGenerationTitle": "Génération de requête ES|QL", "xpack.elasticAssistantPlugin.assistant.title": "Assistant d'IA d'Elastic", "xpack.elasticAssistantPlugin.attackDiscovery.defaultAttackDiscoveryGraph.nodes.retriever.helpers.throwIfErrorCountsExceeded.maxGenerationAttemptsErrorMessage": "Nombre maximum de tentatives de génération ({generationAttempts}) atteint. Essayez d'envoyer un nombre d'alertes moins élevé à ce modèle.", @@ -35056,8 +35062,7 @@ "xpack.securitySolution.assistant.commentActions.viewAPMTraceLabel": "Voir la trace APM pour ce message", "xpack.securitySolution.assistant.content.promptContexts.indexTitle": "index", "xpack.securitySolution.assistant.content.promptContexts.viewTitle": "vue", - "xpack.securitySolution.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown": "Ajoutez votre description, les actions que vous recommandez ainsi que les étapes de triage à puces. Utilisez les données \"MITRE ATT&CK\" fournies pour ajouter du contexte et des recommandations de MITRE ainsi que des liens hypertexte vers les pages pertinentes sur le site web de MITRE. Assurez-vous d'inclure les scores de risque de l'utilisateur et de l'hôte du contexte. Votre réponse doit inclure des étapes qui pointent vers les fonctionnalités spécifiques d'Elastic Security, y compris les actions de réponse du terminal, l'intégration OSQuery Manager d'Elastic Agent (avec des exemples de requêtes OSQuery), des analyses de timeline et d'entités, ainsi qu'un lien pour toute la documentation Elastic Security pertinente.", - "xpack.securitySolution.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries": "Évaluer l’événement depuis le contexte ci-dessus et formater soigneusement la sortie en syntaxe Markdown pour mon cas Elastic Security.", + "xpack.securitySolution.assistant.conversationMigrationStatus.title": "Les conversations de stockage local ont été persistées avec succès.", "xpack.securitySolution.assistant.quickPrompts.alertSummarizationPrompt": "En tant qu’expert en opérations de sécurité et en réponses aux incidents, décomposer l’alerte jointe et résumer ce qu’elle peut impliquer pour mon organisation.", "xpack.securitySolution.assistant.quickPrompts.alertSummarizationTitle": "Synthèse de l’alerte", "xpack.securitySolution.assistant.quickPrompts.AutomationPrompt": "Quelle intégration d’Elastic Agent activée par Fleet dois-je utiliser pour collecter des logs et des évènements de :", @@ -37002,7 +37007,6 @@ "xpack.securitySolution.detectionEngine.ruleManagement.createRule.threatMappingField.requiredError": "Au moins une correspondance d'indicateur est requise.", "xpack.securitySolution.detectionEngine.ruleManagement.detectionRulesConversationId": "Règles de détection", "xpack.securitySolution.detectionEngine.ruleManagement.detectionRulesCreateEditFormConversationId": "Formulaire de création des règles de détection", - "xpack.securitySolution.detectionEngine.ruleManagement.explainThenSummarizeRuleDetails": "Veuillez expliquer les règles sélectionnées ci-dessus. Pour chaque règle : indiquez pourquoi la règle est pertinente, la requête telle qu'elle est publiée dans le référentiel de règles de détection d'Elastic, ainsi qu'une explication approfondie de celle-ci et ce qu'elle implique généralement pour une organisation si elle est détectée.", "xpack.securitySolution.detectionEngine.ruleManagement.fields.from.helpText": "Expression mathématique de datage, par ex. \"now\", \"now-3d\", \"now+2m\".", "xpack.securitySolution.detectionEngine.ruleManagement.fields.from.label": "De", "xpack.securitySolution.detectionEngine.ruleManagement.fields.interval.helpText": "Les règles s'exécutent de façon régulière et détectent les alertes dans la période de temps spécifiée.", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index a7a6df50d75e3..fdda24050cc30 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -7529,7 +7529,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.dataQualityDashboardConversationId": "データ品質ダッシュボード", "securitySolutionPackages.ecsDataQualityDashboard.dataQualityPromptContextPill": "データ品質({indexName})", "securitySolutionPackages.ecsDataQualityDashboard.dataQualityPromptContextPillTooltip": "このデータ品質レポートをコンテキストとして追加", - "securitySolutionPackages.ecsDataQualityDashboard.dataQualitySuggestedUserPrompt": "上記の結果を説明し、一部のオプションを記述して非互換性を修正します。", "securitySolutionPackages.ecsDataQualityDashboard.defaultPanelTitle": "インデックスマッピングの確認", "securitySolutionPackages.ecsDataQualityDashboard.detectionEngineRulesWontWorkMessage": "❌ これらのフィールドを参照する検出エンジンルールが正常に一致しない場合がある", "securitySolutionPackages.ecsDataQualityDashboard.docs": "ドキュメント", @@ -16381,6 +16380,13 @@ "xpack.elasticAssistant.prompts.getPromptsError": "プロンプト取得エラー", "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "Elastic AI Assistantはエンタープライズユーザーのみご利用いただけます。 この機能を使用するには、ライセンスをアップグレードしてください。", "xpack.elasticAssistantPlugin.assistant.apiErrorTitle": "メッセージの送信中にエラーが発生しました。", + "xpack.elasticAssistantPlugin.assistant.contentReferences.knowledgeBaseEntryReference.label": "ナレッジベースエントリ", + "xpack.elasticAssistantPlugin.assistant.contentReferences.securityAlertReference.label": "アラートを表示", + "xpack.elasticAssistantPlugin.assistant.contentReferences.securityAlertsPageReference.label": "アラートを表示", + "xpack.elasticAssistantPlugin.assistant.getComments.assistant": "アシスタント", + "xpack.elasticAssistantPlugin.assistant.getComments.at": "at: {timestamp}", + "xpack.elasticAssistantPlugin.assistant.getComments.system": "システム", + "xpack.elasticAssistantPlugin.assistant.getComments.you": "あなた", "xpack.elasticAssistantPlugin.assistant.quickPrompts.esqlQueryGenerationTitle": "ES|QLクエリ生成", "xpack.elasticAssistantPlugin.attackDiscovery.defaultAttackDiscoveryGraph.nodes.retriever.helpers.throwIfErrorCountsExceeded.maxGenerationAttemptsErrorMessage": "最大生成試行回数({generationAttempts})に達しました。このモデルに送信するアラートの数を減らしてください。", "xpack.elasticAssistantPlugin.attackDiscovery.defaultAttackDiscoveryGraph.nodes.retriever.helpers.throwIfErrorCountsExceeded.maxHallucinationFailuresErrorMessage": "最大ハルシネーション失敗回数({hallucinationFailures})に達しました。このモデルに送信するアラートの数を減らしてください。", @@ -35090,8 +35096,7 @@ "xpack.securitySolution.assistant.commentActions.viewAPMTraceLabel": "このメッセージのAPMトレースを表示", "xpack.securitySolution.assistant.content.promptContexts.indexTitle": "インデックス", "xpack.securitySolution.assistant.content.promptContexts.viewTitle": "表示", - "xpack.securitySolution.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown": "説明、推奨されるアクション、箇条書きのトリアージステップを追加します。提供されたMITRE ATT&CKデータを使用して、MITREからのコンテキストや推奨事項を追加し、MITREのWebサイトの関連ページにハイパーリンクを貼ります。コンテキストのユーザーとホストのリスクスコアデータを必ず含めてください。回答には、エンドポイント対応アクション、ElasticエージェントOSQueryマネージャー統合(osqueryクエリーの例を付けて)、タイムライン、エンティティ分析など、Elasticセキュリティ固有の機能を指す手順を含め、関連するElasticセキュリティのドキュメントすべてにリンクしてください。", - "xpack.securitySolution.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries": "上記のコンテキストからイベントを評価し、Elasticセキュリティのケース用に、出力をマークダウン構文で正しく書式設定してください。", + "xpack.securitySolution.assistant.conversationMigrationStatus.title": "ローカルストレージ会話は正常に永続しました。", "xpack.securitySolution.assistant.quickPrompts.alertSummarizationPrompt": "セキュリティ運用とインシデント対応のエキスパートとして、添付されたアラートの内訳を説明し、それが私の組織にとって何を意味するのかを要約してください。", "xpack.securitySolution.assistant.quickPrompts.alertSummarizationTitle": "アラート要約", "xpack.securitySolution.assistant.quickPrompts.AutomationPrompt": "ログやイベントの収集には、どのFleet対応Elasticエージェント統合を使用すべきですか。", @@ -37039,7 +37044,6 @@ "xpack.securitySolution.detectionEngine.ruleManagement.createRule.threatMappingField.requiredError": "1 つ以上のインジケーター一致が必要です。", "xpack.securitySolution.detectionEngine.ruleManagement.detectionRulesConversationId": "検出ルール", "xpack.securitySolution.detectionEngine.ruleManagement.detectionRulesCreateEditFormConversationId": "検出ルール作成フォーム", - "xpack.securitySolution.detectionEngine.ruleManagement.explainThenSummarizeRuleDetails": "上記の選択したルールを説明してください。各ルールについて、関連する理由、Elasticの検出ルールリポジトリで公開されているクエリとその詳細な説明、検出された場合の組織にとっての一般的な意味を強調します。", "xpack.securitySolution.detectionEngine.ruleManagement.fields.from.helpText": "日付演算式。例:now、now-3d、now+2mなど。", "xpack.securitySolution.detectionEngine.ruleManagement.fields.from.label": "開始:", "xpack.securitySolution.detectionEngine.ruleManagement.fields.interval.helpText": "ルールを定期的に実行し、指定の時間枠内でアラートを検出します。", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index a9b23b18146ba..bcc5d8f1d5380 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -7524,7 +7524,6 @@ "securitySolutionPackages.ecsDataQualityDashboard.dataQualityDashboardConversationId": "数据质量仪表板", "securitySolutionPackages.ecsDataQualityDashboard.dataQualityPromptContextPill": "数据质量 ({indexName})", "securitySolutionPackages.ecsDataQualityDashboard.dataQualityPromptContextPillTooltip": "将此数据质量报告添加为上下文", - "securitySolutionPackages.ecsDataQualityDashboard.dataQualitySuggestedUserPrompt": "解释上述结果,并说明某些选项以解决不兼容问题。", "securitySolutionPackages.ecsDataQualityDashboard.defaultPanelTitle": "检查索引映射", "securitySolutionPackages.ecsDataQualityDashboard.detectionEngineRulesWontWorkMessage": "❌ 引用这些字段的检测引擎规则可能无法与其正确匹配", "securitySolutionPackages.ecsDataQualityDashboard.docs": "文档", @@ -16377,6 +16376,13 @@ "xpack.elasticAssistant.prompts.getPromptsError": "提取提示时出错", "xpack.elasticAssistant.securityAssistant.content.prompts.welcome.enterprisePrompt": "Elastic AI 助手仅对企业用户可用。请升级许可证以使用此功能。", "xpack.elasticAssistantPlugin.assistant.apiErrorTitle": "发送消息时出错。", + "xpack.elasticAssistantPlugin.assistant.contentReferences.knowledgeBaseEntryReference.label": "知识库条目", + "xpack.elasticAssistantPlugin.assistant.contentReferences.securityAlertReference.label": "查看告警", + "xpack.elasticAssistantPlugin.assistant.contentReferences.securityAlertsPageReference.label": "查看告警", + "xpack.elasticAssistantPlugin.assistant.getComments.assistant": "助手", + "xpack.elasticAssistantPlugin.assistant.getComments.at": "时间:{timestamp}", + "xpack.elasticAssistantPlugin.assistant.getComments.system": "系统", + "xpack.elasticAssistantPlugin.assistant.getComments.you": "您", "xpack.elasticAssistantPlugin.assistant.quickPrompts.esqlQueryGenerationTitle": "ES|QL 查询生成", "xpack.elasticAssistantPlugin.attackDiscovery.defaultAttackDiscoveryGraph.nodes.retriever.helpers.throwIfErrorCountsExceeded.maxGenerationAttemptsErrorMessage": "已达到最大生成尝试次数 ({generationAttempts})。尝试向此模型发送更少的告警。", "xpack.elasticAssistantPlugin.attackDiscovery.defaultAttackDiscoveryGraph.nodes.retriever.helpers.throwIfErrorCountsExceeded.maxHallucinationFailuresErrorMessage": "已达到最大幻觉失败次数 ({hallucinationFailures})。尝试向此模型发送更少的告警。", @@ -35079,8 +35085,7 @@ "xpack.securitySolution.assistant.commentActions.viewAPMTraceLabel": "查看此消息的 APM 跟踪", "xpack.securitySolution.assistant.content.promptContexts.indexTitle": "索引", "xpack.securitySolution.assistant.content.promptContexts.viewTitle": "视图", - "xpack.securitySolution.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown": "添加描述、建议操作和带项目符号的分类步骤。使用提供的 MITRE ATT&CK 数据以从 MITRE 添加更多上下文和建议,以及指向 MITRE 网站上的相关页面的超链接。确保包括上下文中的用户和主机风险分数数据。您的响应应包含指向 Elastic Security 特定功能的步骤,包括终端响应操作、Elastic 代理 OSQuery 管理器集成(带示例 osquery 查询)、时间线和实体分析,以及所有相关 Elastic Security 文档的链接。", - "xpack.securitySolution.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries": "评估来自上述上下文的事件,并以用于我的 Elastic Security 案例的 Markdown 语法对您的输出进行全面格式化。", + "xpack.securitySolution.assistant.conversationMigrationStatus.title": "已成功保持本地存储对话。", "xpack.securitySolution.assistant.quickPrompts.alertSummarizationPrompt": "作为安全运营和事件响应领域的专家,提供附加告警的细目并简要说明它对我所在组织可能的影响。", "xpack.securitySolution.assistant.quickPrompts.alertSummarizationTitle": "告警汇总", "xpack.securitySolution.assistant.quickPrompts.AutomationPrompt": "我应使用哪个启用 Fleet 的 Elastic 代理集成从以下项中收集日志和事件:", @@ -37027,7 +37032,6 @@ "xpack.securitySolution.detectionEngine.ruleManagement.createRule.threatMappingField.requiredError": "至少需要一个指标匹配。", "xpack.securitySolution.detectionEngine.ruleManagement.detectionRulesConversationId": "检测规则", "xpack.securitySolution.detectionEngine.ruleManagement.detectionRulesCreateEditFormConversationId": "检测规则创建了表单", - "xpack.securitySolution.detectionEngine.ruleManagement.explainThenSummarizeRuleDetails": "请解释上面的选定规则。对于每个规则,重点说明它们为什么相关、在 Elastic 的检测规则存储库中发布的查询并提供深入解释,以及它们通常对组织的影响(如果检测到)。", "xpack.securitySolution.detectionEngine.ruleManagement.fields.from.helpText": "日期数学表达式,如“now”、“now-3d”、“now+2m”。", "xpack.securitySolution.detectionEngine.ruleManagement.fields.from.label": "自", "xpack.securitySolution.detectionEngine.ruleManagement.fields.interval.helpText": "规则定期运行并检测指定时间范围内的告警。", diff --git a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/actions/chat/index.test.tsx b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/actions/chat/index.test.tsx index 33e52a398e3c9..d7fc944ded5f2 100644 --- a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/actions/chat/index.test.tsx +++ b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/actions/chat/index.test.tsx @@ -9,21 +9,225 @@ import React from 'react'; import { screen, render } from '@testing-library/react'; import { ChatAction } from '.'; +import { TestDataQualityProviders } from '../../mock/test_providers/test_providers'; +import { NewChat, useFindPrompts } from '@kbn/elastic-assistant'; import { - TestDataQualityProviders, - TestExternalProviders, -} from '../../mock/test_providers/test_providers'; + DATA_QUALITY_PROMPT_CONTEXT_PILL, + DATA_QUALITY_SUGGESTED_USER_PROMPT, +} from '../../translations'; +import { getFormattedCheckTime } from '../../data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time'; + +jest.mock('@kbn/elastic-assistant', () => ({ + NewChat: jest.fn(({ children }) => ( + + )), + useFindPrompts: jest.fn().mockReturnValue({ + data: { prompts: [] }, + }), +})); + +const useFindPromptsMock = useFindPrompts as unknown as jest.Mock< + Pick, 'data'> +>; +const NewChatMock = NewChat as jest.MockedFunction; describe('ChatAction', () => { - it('should render new chat link', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render new chat link', async () => { render( - + + + + ); + + expect(screen.getByTestId('newChatLink')).toHaveTextContent('Ask Assistant'); + }); + + it('should pass correct props to NewChat with minimal required props', () => { + const markdownComment = 'test markdown comment'; + const indexName = 'test-index-name'; + + render( + + + + ); + + expect(NewChatMock).toHaveBeenCalledWith( + expect.objectContaining({ + asLink: true, + category: 'data-quality-dashboard', + conversationTitle: '--', + description: expect.any(String), + getPromptContext: expect.any(Function), + suggestedUserPrompt: expect.any(String), + tooltip: expect.any(String), + isAssistantEnabled: true, + iconType: null, + }), + {} + ); + }); + + it('should pass index name in NewChat description prop', () => { + const markdownComment = 'test markdown'; + const indexName = 'specific-test-index'; + + render( + + + + ); + + const callArgs = NewChatMock.mock.calls[0][0]; + + expect(callArgs.description).toBe(DATA_QUALITY_PROMPT_CONTEXT_PILL(indexName)); + }); + + describe('when checkedAt timestamp is provided', () => { + it('should pass correct conversation title with formatted timestamp', () => { + const markdownComment = 'test markdown'; + const indexName = 'test-index'; + const checkedAt = 1640995200000; + + render( + + + + ); + + const expectedTitle = `${indexName} - ${getFormattedCheckTime(checkedAt)}`; + + expect(NewChatMock).toHaveBeenCalledWith( + expect.objectContaining({ + conversationTitle: expectedTitle, + }), + {} + ); + }); + }); + + describe('when checkedAt timestamp is not provided', () => { + it('should replace conversation title with empty stat "--" fallback', () => { + const markdownComment = 'test markdown'; + const indexName = 'test-index'; + + render( - + - + ); + + expect(NewChatMock).toHaveBeenCalledWith( + expect.objectContaining({ + conversationTitle: '--', + }), + {} + ); + }); + }); + + describe('when dataQualityAnalysis prompt is available', () => { + it('should use it', () => { + const dqdPrompt = 'Custom data quality analysis prompt'; + const markdownComment = 'test markdown'; + const indexName = 'test-index'; + + useFindPromptsMock.mockReturnValue({ + data: { + prompts: [ + { promptId: 'dataQualityAnalysis', prompt: dqdPrompt }, + { promptId: 'other', prompt: 'Other prompt' }, + ], + }, + }); + + render( + + + + ); + + expect(NewChatMock).toHaveBeenCalledWith( + expect.objectContaining({ + suggestedUserPrompt: dqdPrompt, + }), + {} + ); + }); + }); + + describe('when dataQualityAnalysis prompt is not available', () => { + it('should use fallback prompt', () => { + const markdownComment = 'test markdown'; + const indexName = 'test-index'; + + (useFindPrompts as jest.Mock).mockReturnValue({ + data: { + prompts: [{ promptId: 'other', prompt: 'Other prompt' }], + }, + }); + + render( + + + + ); + + expect(NewChatMock).toHaveBeenCalledWith( + expect.objectContaining({ + suggestedUserPrompt: DATA_QUALITY_SUGGESTED_USER_PROMPT, + }), + {} + ); + }); + }); + + it('should call useFindPrompts hook with correct context and params', () => { + const markdownComment = 'test markdown'; + const indexName = 'test-index'; + + render( + + + ); - expect(screen.getByTestId('newChatLink')).toHaveTextContent('Ask Assistant'); + expect(useFindPromptsMock).toHaveBeenCalledWith({ + context: { + isAssistantEnabled: true, + httpFetch: expect.any(Function), + toasts: expect.any(Object), + }, + params: { + prompt_group_id: 'aiAssistant', + prompt_ids: ['dataQualityAnalysis'], + }, + }); + }); + + it('should provide NewChat getPromptContext function that returns markdownComment', async () => { + const markdownComment = 'test markdown comment for context'; + const indexName = 'test-index'; + + render( + + + + ); + + const callArgs = NewChatMock.mock.calls[0][0]; + const getPromptContext = callArgs.getPromptContext; + + await expect(getPromptContext()).resolves.toBe(markdownComment); }); }); diff --git a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/actions/chat/index.tsx b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/actions/chat/index.tsx index 3ecbbee9a2433..a27e441a3240b 100644 --- a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/actions/chat/index.tsx +++ b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/actions/chat/index.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { FC, useCallback, useMemo } from 'react'; -import { NewChat } from '@kbn/elastic-assistant'; +import type { FC } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import { NewChat, useFindPrompts } from '@kbn/elastic-assistant'; import { css } from '@emotion/react'; import { useEuiTheme } from '@elastic/eui'; @@ -40,12 +41,34 @@ interface Props { const ChatActionComponent: FC = ({ indexName, markdownComment, checkedAt }) => { const chatTitle = useMemo(() => { - return `${indexName} - ${getFormattedCheckTime(checkedAt)}`; + const checkedAtFormatted = getFormattedCheckTime(checkedAt); + return checkedAt && indexName ? `${indexName} - ${checkedAtFormatted}` : checkedAtFormatted; }, [checkedAt, indexName]); const styles = useStyles(); - const { isAssistantEnabled } = useDataQualityContext(); + const { isAssistantEnabled, httpFetch, toasts } = useDataQualityContext(); + const { + data: { prompts }, + } = useFindPrompts({ + context: { + isAssistantEnabled, + httpFetch, + toasts, + }, + params: { + prompt_group_id: 'aiAssistant', + prompt_ids: ['dataQualityAnalysis'], + }, + }); const getPromptContext = useCallback(async () => markdownComment, [markdownComment]); + + const suggestedUserPrompt = useMemo( + () => + prompts.find(({ promptId }) => promptId === 'dataQualityAnalysis')?.prompt ?? + DATA_QUALITY_SUGGESTED_USER_PROMPT, + [prompts] + ); + return ( = ({ indexName, markdownComment, checkedAt conversationTitle={chatTitle ?? DATA_QUALITY_DASHBOARD_CONVERSATION_ID} description={DATA_QUALITY_PROMPT_CONTEXT_PILL(indexName)} getPromptContext={getPromptContext} - suggestedUserPrompt={DATA_QUALITY_SUGGESTED_USER_PROMPT} + suggestedUserPrompt={suggestedUserPrompt} tooltip={DATA_QUALITY_PROMPT_CONTEXT_PILL_TOOLTIP} isAssistantEnabled={isAssistantEnabled} iconType={null} > - + {ASK_ASSISTANT} diff --git a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.test.ts b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.test.ts index 9956bbc525aa8..efb1d1fc5d236 100644 --- a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.test.ts +++ b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.test.ts @@ -10,6 +10,7 @@ import moment from 'moment-timezone'; moment.tz.setDefault('UTC'); import { getFormattedCheckTime } from './get_formatted_check_time'; +import { EMPTY_STAT } from '../../../../../constants'; describe('getFormattedCheckTime', () => { it('returns formatted check time', () => { @@ -23,4 +24,11 @@ describe('getFormattedCheckTime', () => { expect(formattedCheckTime).toBe('--'); }); }); + + describe('when check time is not provided', () => { + it(`returns ${EMPTY_STAT} string`, () => { + const formattedCheckTime = getFormattedCheckTime(); + expect(formattedCheckTime).toBe(EMPTY_STAT); + }); + }); }); diff --git a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.ts b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.ts index acad51527bace..6acb68e949efa 100644 --- a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.ts +++ b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/data_quality_details/indices_details/pattern/index_check_flyout/utils/get_formatted_check_time.ts @@ -10,4 +10,6 @@ import moment from 'moment'; import { EMPTY_STAT } from '../../../../../constants'; export const getFormattedCheckTime = (checkedAt?: number) => - moment(checkedAt).isValid() ? moment(checkedAt).format('MMM DD, YYYY @ HH:mm:ss') : EMPTY_STAT; + checkedAt && moment(checkedAt).isValid() + ? moment(checkedAt).format('MMM DD, YYYY @ HH:mm:ss') + : EMPTY_STAT; diff --git a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/translations.ts b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/translations.ts index 93150b79138dc..9f9e31e9c9142 100644 --- a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/translations.ts +++ b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/translations.ts @@ -78,10 +78,10 @@ export const DATA_QUALITY_SUBTITLE: string = i18n.translate( ); export const DATA_QUALITY_SUGGESTED_USER_PROMPT = i18n.translate( - 'securitySolutionPackages.ecsDataQualityDashboard.dataQualitySuggestedUserPrompt', + 'securitySolutionPackages.ecsDataQualityDashboard.dataQualitySuggestedUserPromptV2', { defaultMessage: - 'Explain the results above, and describe some options to fix incompatibilities.', + 'Explain the ECS incompatibility results above, and describe some options to fix incompatibilities. In your explanation, include information about remapping fields, reindexing data, and modifying data ingestion pipelines. Also, describe how ES|QL can be used to identify and correct incompatible data, including examples of using RENAME, EVAL, DISSECT, GROK, and CASE functions.', } ); diff --git a/x-pack/solutions/security/packages/security-ai-prompts/src/get_prompt.test.ts b/x-pack/solutions/security/packages/security-ai-prompts/src/get_prompt.test.ts index 61ae5aefc6220..1cb6d37619de5 100644 --- a/x-pack/solutions/security/packages/security-ai-prompts/src/get_prompt.test.ts +++ b/x-pack/solutions/security/packages/security-ai-prompts/src/get_prompt.test.ts @@ -402,6 +402,17 @@ describe('get_prompt', () => { expect(result).toBe('Hello world this is a system prompt for bedrock claude-3-5-sonnet'); }); + + it('finds the default prompt if no provider/model are indicated and no connector details are provided', async () => { + const result = await getPrompt({ + savedObjectsClient, + localPrompts, + promptId: promptDictionary.systemPrompt, + promptGroupId: promptGroupId.aiAssistant, + }); + + expect(result).toEqual('Hello world this is a system prompt no model, no provider'); + }); }); describe('getPromptsByGroupId', () => { @@ -533,5 +544,21 @@ describe('get_prompt', () => { }) ).rejects.toThrow('Prompt not found for promptId: fake-id and promptGroupId: aiAssistant'); }); + + it('finds the default prompt if no provider/model are indicated and no connector details are provided', async () => { + const result = await getPromptsByGroupId({ + savedObjectsClient, + localPrompts, + promptIds: [promptDictionary.systemPrompt], + promptGroupId: promptGroupId.aiAssistant, + }); + + expect(result).toEqual([ + { + promptId: promptDictionary.systemPrompt, + prompt: 'Hello world this is a system prompt no model, no provider', + }, + ]); + }); }); }); diff --git a/x-pack/solutions/security/packages/security-ai-prompts/src/get_prompt.ts b/x-pack/solutions/security/packages/security-ai-prompts/src/get_prompt.ts index edf01bb0392ac..9e122dfec276c 100644 --- a/x-pack/solutions/security/packages/security-ai-prompts/src/get_prompt.ts +++ b/x-pack/solutions/security/packages/security-ai-prompts/src/get_prompt.ts @@ -16,7 +16,7 @@ import { promptSavedObjectType } from './saved_object_mappings'; /** * Get prompts by feature (promptGroupId) * provide either model + provider or connector to avoid additional calls to get connector - * @param actionsClient - actions client + * @param actionsClient - actions client (look up connector if connector is not provided) * @param connector - connector, provide if available. No need to provide model and provider in this case * @param connectorId - connector id * @param localPrompts - local prompts object @@ -138,15 +138,20 @@ export const resolveProviderAndModel = async ({ }: { providedProvider?: string; providedModel?: string; - connectorId: string; - actionsClient: PublicMethodsOf; + connectorId?: string; + actionsClient?: PublicMethodsOf; providedConnector?: Connector; }): Promise<{ provider?: string; model?: string }> => { let model = providedModel; let provider = providedProvider; if (!provider || !model || provider === 'inference') { - const connector = providedConnector ?? (await actionsClient.get({ id: connectorId })); - + let connector = providedConnector; + if (!connector && connectorId != null && actionsClient) { + connector = await actionsClient.get({ id: connectorId }); + } + if (!connector) { + return {}; + } if (provider === 'inference' && connector.config) { provider = connector.config.provider || provider; model = connector.config.providerConfig?.model_id || model; diff --git a/x-pack/solutions/security/packages/security-ai-prompts/src/types.ts b/x-pack/solutions/security/packages/security-ai-prompts/src/types.ts index ee2ba36b1d47b..c66b6a4e5674e 100644 --- a/x-pack/solutions/security/packages/security-ai-prompts/src/types.ts +++ b/x-pack/solutions/security/packages/security-ai-prompts/src/types.ts @@ -24,9 +24,9 @@ export interface Prompt { export type PromptArray = Array<{ promptId: string; prompt: string }>; export interface GetPromptArgs { - actionsClient: PublicMethodsOf; + actionsClient?: PublicMethodsOf; connector?: Connector; - connectorId: string; + connectorId?: string; localPrompts: Prompt[]; model?: string; promptId: string; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts index 14705c3bce78a..9103c5b8260a3 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts @@ -49,6 +49,7 @@ import { ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND, PerformKnowledgeBaseEntryBulkActionRequestBody, PostEvaluateRequestBodyInput, + ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND, } from '@kbn/elastic-assistant-common'; import { getAppendConversationMessagesSchemaMock, @@ -167,6 +168,13 @@ export const getCurrentUserPromptsRequest = () => path: ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND, }); +export const getCurrentUserSecurityAIPromptsRequest = () => + requestMock.create({ + method: 'get', + path: ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND, + query: { prompt_group_id: 'aiAssistant', prompt_ids: ['systemPrompt'] }, + }); + export const getCurrentUserAlertSummaryRequest = () => requestMock.create({ method: 'get', diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts index 331c36212bde1..703e29c8f30a4 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/local_prompt_object.ts @@ -26,6 +26,9 @@ import { DEFEND_INSIGHTS, ALERT_SUMMARY_500, ALERT_SUMMARY_SYSTEM_PROMPT, + RULE_ANALYSIS, + DATA_QUALITY_ANALYSIS, + ALERT_EVALUATION, } from './prompts'; export const promptGroupId = { @@ -38,6 +41,9 @@ export const promptGroupId = { }; export const promptDictionary = { + alertEvaluation: `alertEvaluation`, + dataQualityAnalysis: 'dataQualityAnalysis', + ruleAnalysis: 'ruleAnalysis', alertSummary: `alertSummary`, alertSummarySystemPrompt: `alertSummarySystemPrompt`, systemPrompt: `systemPrompt`, @@ -61,6 +67,7 @@ export const promptDictionary = { defendInsightsIncompatibleAntivirusEventsEndpointId: 'defendInsights-incompatibleAntivirusEventsEndpointId', defendInsightsIncompatibleAntivirusEventsValue: 'defendInsights-incompatibleAntivirusEventsValue', + // context prompts }; export const localPrompts: Prompt[] = [ @@ -259,4 +266,25 @@ export const localPrompts: Prompt[] = [ default: ALERT_SUMMARY_SYSTEM_PROMPT, }, }, + { + promptId: promptDictionary.alertEvaluation, + promptGroupId: promptGroupId.aiAssistant, + prompt: { + default: ALERT_EVALUATION, + }, + }, + { + promptId: promptDictionary.dataQualityAnalysis, + promptGroupId: promptGroupId.aiAssistant, + prompt: { + default: DATA_QUALITY_ANALYSIS, + }, + }, + { + promptId: promptDictionary.ruleAnalysis, + promptGroupId: promptGroupId.aiAssistant, + prompt: { + default: RULE_ANALYSIS, + }, + }, ]; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts index 2a1682ea19272..94ac4c2af1579 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/prompt/prompts.ts @@ -218,3 +218,40 @@ export const ALERT_SUMMARY_SYSTEM_PROMPT = '\n' + 'The response should look like this:\n' + '{{"summary":"Markdown-formatted summary text.","recommendedActions":"Markdown-formatted action list starting with a ### header."}}'; + +export const RULE_ANALYSIS = + 'Please provide a comprehensive analysis of each selected Elastic Security detection rule. For each rule, include:\n' + + '- The rule name and a brief summary of its purpose.\n' + + '- The full detection query as published in Elastic’s official detection rules repository.\n' + + '- An in-depth explanation of how the query works, including key fields, logic, and detection techniques.\n' + + '- The relevance of the rule to modern threats or attack techniques (e.g., MITRE ATT&CK mapping).\n' + + '- Typical implications and recommended response actions for an organization if this rule triggers.\n' + + '- Any notable false positive considerations or tuning recommendations.\n' + + 'Format your response using markdown with clear headers for each rule, code blocks for queries, and concise bullet points for explanations.'; + +export const DATA_QUALITY_ANALYSIS = + 'Explain the ECS incompatibility results above, and describe some options to fix incompatibilities. In your explanation, include information about remapping fields, reindexing data, and modifying data ingestion pipelines. Also, describe how ES|QL can be used to identify and correct incompatible data, including examples of using RENAME, EVAL, DISSECT, GROK, and CASE functions.'; + +export const ALERT_EVALUATION = `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, including user and host risk scores from the provided context. + - 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., 📝, 🛡️, 🔍, 📚). +`; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts index f856491958537..e41644d550abc 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts @@ -7,6 +7,7 @@ import type { Logger } from '@kbn/core/server'; +import { findSecurityAIPromptsRoute } from './security_ai_prompts/find_prompts'; import { findAlertSummaryRoute } from './alert_summary/find_route'; import { cancelAttackDiscoveryRoute } from './attack_discovery/post/cancel/cancel_attack_discovery'; import { findAttackDiscoveriesRoute } from './attack_discovery/get/find_attack_discoveries'; @@ -108,6 +109,9 @@ export const registerRoutes = ( bulkPromptsRoute(router, logger); findPromptsRoute(router, logger); + // Security AI Prompts + findSecurityAIPromptsRoute(router, logger); + // Anonymization Fields bulkActionAnonymizationFieldsRoute(router, logger); findAnonymizationFieldsRoute(router, logger); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/security_ai_prompts/find_prompts.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/security_ai_prompts/find_prompts.test.ts new file mode 100644 index 0000000000000..1942aaf03fd2b --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/security_ai_prompts/find_prompts.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { getCurrentUserSecurityAIPromptsRequest, requestMock } from '../../__mocks__/request'; +import { ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND } from '@kbn/elastic-assistant-common'; +import { serverMock } from '../../__mocks__/server'; +import { requestContextMock } from '../../__mocks__/request_context'; +import { findSecurityAIPromptsRoute } from './find_prompts'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import type { AuthenticatedUser } from '@kbn/core-security-common'; +import { getPromptsByGroupId } from '../../lib/prompt'; +import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock'; +jest.mock('../../lib/prompt'); +const mockResponse = [{ promptId: 'systemPrompt', prompt: 'This is a prompt' }]; +describe('Find security AI prompts route', () => { + let server: ReturnType; + let { context } = requestContextMock.createTools(); + let logger: ReturnType; + + beforeEach(async () => { + server = serverMock.create(); + ({ context } = requestContextMock.createTools()); + const mockUser1 = { + username: 'my_username', + authentication_realm: { + type: 'my_realm_type', + name: 'my_realm_name', + }, + } as AuthenticatedUser; + + (getPromptsByGroupId as jest.Mock).mockResolvedValue(Promise.resolve(mockResponse)); + context.elasticAssistant.getCurrentUser.mockResolvedValueOnce({ + username: 'my_username', + authentication_realm: { + type: 'my_realm_type', + name: 'my_realm_name', + }, + } as AuthenticatedUser); + (context.elasticAssistant.actions.getActionsClientWithRequest as jest.Mock) = jest + .fn() + .mockReturnValueOnce(actionsClientMock.create()); + logger = loggingSystemMock.createLogger(); + context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1); + findSecurityAIPromptsRoute(server.router, logger); + }); + + describe('status codes', () => { + it('returns 200', async () => { + const response = await server.inject( + getCurrentUserSecurityAIPromptsRequest(), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ prompts: mockResponse }); + }); + + it('catches error if search throws error', async () => { + (getPromptsByGroupId as jest.Mock).mockRejectedValueOnce(new Error('Test error')); + const response = await server.inject( + getCurrentUserSecurityAIPromptsRequest(), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Test error', + status_code: 500, + }); + }); + }); + + describe('request validation', () => { + it('allows optional query params', async () => { + const request = requestMock.create({ + method: 'get', + path: ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND, + query: { + prompt_group_id: 'aiAssistant', + prompt_ids: ['systemPrompt'], + connector_id: '123', + }, + }); + const result = server.validate(request); + + expect(result.ok).toHaveBeenCalled(); + }); + + it('ignores unknown query params', async () => { + const request = requestMock.create({ + method: 'get', + path: ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND, + query: { + prompt_group_id: 'aiAssistant', + prompt_ids: ['systemPrompt'], + invalid_value: 'test 1', + }, + }); + const result = server.validate(request); + + expect(result.ok).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/security_ai_prompts/find_prompts.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/security_ai_prompts/find_prompts.ts new file mode 100644 index 0000000000000..596b747d2d582 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/security_ai_prompts/find_prompts.ts @@ -0,0 +1,92 @@ +/* + * 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 { IKibanaResponse, Logger } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import { + API_VERSIONS, + ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND, + FindSecurityAIPromptsRequestQuery, + FindSecurityAIPromptsResponse, +} from '@kbn/elastic-assistant-common'; +import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common'; +import { getPromptsByGroupId } from '../../lib/prompt'; +import { ElasticAssistantPluginRouter } from '../../types'; +import { buildResponse } from '../utils'; +import { performChecks } from '../helpers'; + +export const findSecurityAIPromptsRoute = (router: ElasticAssistantPluginRouter, logger: Logger) => + router.versioned + .get({ + access: 'public', + path: ELASTIC_AI_ASSISTANT_SECURITY_AI_PROMPTS_URL_FIND, + security: { + authz: { + requiredPrivileges: ['elasticAssistant'], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: { + query: buildRouteValidationWithZod(FindSecurityAIPromptsRequestQuery), + }, + response: { + 200: { + body: { custom: buildRouteValidationWithZod(FindSecurityAIPromptsResponse) }, + }, + }, + }, + }, + async ( + context, + request, + response + ): Promise> => { + const assistantResponse = buildResponse(response); + + try { + const { query } = request; + const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']); + // Perform license and authenticated user checks + const checkResponse = await performChecks({ + context: ctx, + request, + response, + }); + if (!checkResponse.isSuccess) { + return checkResponse.response; + } + const actions = ctx.elasticAssistant.actions; + const actionsClient = await actions.getActionsClientWithRequest(request); + const savedObjectsClient = ctx.elasticAssistant.savedObjectsClient; + + const prompts = await getPromptsByGroupId({ + actionsClient, + connectorId: query.connector_id, + promptGroupId: query.prompt_group_id, + promptIds: query.prompt_ids, + savedObjectsClient, + }); + + return response.ok({ + body: { + prompts, + }, + }); + } catch (err) { + const error = transformError(err); + return assistantResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompt_contexts/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompt_contexts/index.tsx index 8c05cd0c9f68e..eea83373cac49 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompt_contexts/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompt_contexts/index.tsx @@ -10,13 +10,11 @@ import * as i18nDataQuality from '@kbn/ecs-data-quality-dashboard'; import * as i18n from './translations'; import * as i18nDetections from '../../../detection_engine/common/translations'; import * as i18nEventDetails from '../../../common/components/event_details/translations'; -import * as i18nUserPrompts from '../prompts/user/translations'; export const PROMPT_CONTEXT_ALERT_CATEGORY = 'alert'; export const PROMPT_CONTEXT_EVENT_CATEGORY = 'event'; export const PROMPT_CONTEXT_DETECTION_RULES_CATEGORY = 'detection-rules'; export const DATA_QUALITY_DASHBOARD_CATEGORY = 'data-quality-dashboard'; -export const KNOWLEDGE_BASE_CATEGORY = 'knowledge-base'; /** * Global list of PromptContexts intended to be used throughout Security Solution. @@ -24,14 +22,15 @@ export const KNOWLEDGE_BASE_CATEGORY = 'knowledge-base'; * a unique set of categories to reference since the PromptContexts available on * useAssistantContext are dynamic (not globally registered). */ -export const PROMPT_CONTEXTS: Record = { +export const getPromptContexts = ( + prompts: Record +): Record => ({ /** * Alert summary view context, made available on the alert details flyout */ [PROMPT_CONTEXT_ALERT_CATEGORY]: { category: PROMPT_CONTEXT_ALERT_CATEGORY, - suggestedUserPrompt: - i18nUserPrompts.EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N, + suggestedUserPrompt: prompts[PROMPT_CONTEXT_ALERT_CATEGORY], description: i18nEventDetails.ALERT_SUMMARY_CONTEXT_DESCRIPTION(i18n.VIEW), tooltip: i18nEventDetails.ALERT_SUMMARY_VIEW_CONTEXT_TOOLTIP, }, @@ -40,8 +39,7 @@ export const PROMPT_CONTEXTS: Record { + beforeEach(() => { + jest.clearAllMocks(); + (useFindPrompts as jest.Mock).mockReturnValue({ data: { prompts: mockPrompts } }); + }); + + it('calls getPromptContexts with the correct prompts mapped by category', () => { + const params = {} as unknown as UseFindPromptContextsParams; + + renderHook(() => useFindPromptContexts(params)); + + const mockReturn = { + alert: { + category: 'alert', + description: 'Alert (from view)', + suggestedUserPrompt: 'ALERT EVALUATION', + tooltip: 'Add this alert as context', + }, + 'data-quality-dashboard': { + category: 'data-quality-dashboard', + description: 'Data Quality (index)', + suggestedUserPrompt: 'DATA QUALITY ANALYSIS', + tooltip: 'Add this Data Quality report as context', + }, + 'detection-rules': { + category: 'detection-rules', + description: 'Selected Detection Rules', + suggestedUserPrompt: 'RULE ANALYSIS', + tooltip: 'Add this alert as context', + }, + event: { + category: 'event', + description: 'Event (from view)', + suggestedUserPrompt: 'ALERT EVALUATION', + tooltip: 'Add this event as context', + }, + }; + const { result } = renderHook(() => useFindPromptContexts(params)); + expect(result.current).toStrictEqual(mockReturn); + }); + + it('uses correct fallback values when the API does not contain the expected results', () => { + (useFindPrompts as jest.Mock).mockReturnValue({ data: { prompts: [] } }); + const params = {} as unknown as UseFindPromptContextsParams; + + renderHook(() => useFindPromptContexts(params)); + + const mockReturn = { + alert: { + category: 'alert', + description: 'Alert (from view)', + suggestedUserPrompt: EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE, + tooltip: 'Add this alert as context', + }, + 'data-quality-dashboard': { + category: 'data-quality-dashboard', + description: 'Data Quality (index)', + suggestedUserPrompt: DATA_QUALITY_SUGGESTED_USER_PROMPT, + tooltip: 'Add this Data Quality report as context', + }, + 'detection-rules': { + category: 'detection-rules', + description: 'Selected Detection Rules', + suggestedUserPrompt: EXPLAIN_THEN_SUMMARIZE_RULE_DETAILS, + tooltip: 'Add this alert as context', + }, + event: { + category: 'event', + description: 'Event (from view)', + suggestedUserPrompt: EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE, + tooltip: 'Add this event as context', + }, + }; + const { result } = renderHook(() => useFindPromptContexts(params)); + expect(result.current).toStrictEqual(mockReturn); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompt_contexts/use_find_prompt_contexts.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompt_contexts/use_find_prompt_contexts.tsx new file mode 100644 index 0000000000000..aa7774c26869b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompt_contexts/use_find_prompt_contexts.tsx @@ -0,0 +1,52 @@ +/* + * 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 { useFindPrompts } from '@kbn/elastic-assistant'; +import type { HttpHandler } from '@kbn/core-http-browser'; +import type { IToasts } from '@kbn/core-notifications-browser'; +import type { FindSecurityAIPromptsRequestQuery } from '@kbn/elastic-assistant-common'; +import { DATA_QUALITY_SUGGESTED_USER_PROMPT } from '@kbn/ecs-data-quality-dashboard'; +import { EXPLAIN_THEN_SUMMARIZE_RULE_DETAILS } from '../../../detection_engine/common/translations'; +import { EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE } from '../prompts/user/translations'; +import { + DATA_QUALITY_DASHBOARD_CATEGORY, + getPromptContexts, + PROMPT_CONTEXT_ALERT_CATEGORY, + PROMPT_CONTEXT_DETECTION_RULES_CATEGORY, + PROMPT_CONTEXT_EVENT_CATEGORY, +} from '.'; +export interface UseFindPromptContextsParams { + context: { + isAssistantEnabled: boolean; + httpFetch: HttpHandler; + toasts: IToasts; + }; + signal?: AbortSignal | undefined; + params: FindSecurityAIPromptsRequestQuery; +} + +export const useFindPromptContexts = (payload: UseFindPromptContextsParams) => { + const { + data: { prompts }, + } = useFindPrompts(payload); + + const PROMPT_CONTEXTS = getPromptContexts({ + [PROMPT_CONTEXT_ALERT_CATEGORY]: + prompts.find(({ promptId }) => promptId === 'alertEvaluation')?.prompt ?? + EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE, + [PROMPT_CONTEXT_EVENT_CATEGORY]: + prompts.find(({ promptId }) => promptId === 'alertEvaluation')?.prompt ?? + EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE, + [DATA_QUALITY_DASHBOARD_CATEGORY]: + prompts.find(({ promptId }) => promptId === 'dataQualityAnalysis')?.prompt ?? + DATA_QUALITY_SUGGESTED_USER_PROMPT, + [PROMPT_CONTEXT_DETECTION_RULES_CATEGORY]: + prompts.find(({ promptId }) => promptId === 'ruleAnalysis')?.prompt ?? + EXPLAIN_THEN_SUMMARIZE_RULE_DETAILS, + }); + return PROMPT_CONTEXTS; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompts/user/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompts/user/translations.ts index 8892444351832..9c9d50abfa9ac 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompts/user/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/content/prompts/user/translations.ts @@ -7,20 +7,31 @@ import { i18n } from '@kbn/i18n'; -export const THEN_SUMMARIZE_SUGGESTED_KQL_AND_EQL_QUERIES = i18n.translate( - 'xpack.securitySolution.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries', +export const EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE = i18n.translate( + 'xpack.securitySolution.assistant.content.prompts.investigationGuide', { - defaultMessage: - 'Evaluate the event from the context above and format your output neatly in markdown syntax for my Elastic Security case.', + defaultMessage: `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, including user and host risk scores from the provided context. + - 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 FINALLY_SUGGEST_INVESTIGATION_GUIDE_AND_FORMAT_AS_MARKDOWN = i18n.translate( - 'xpack.securitySolution.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown', - { - defaultMessage: `Add your description, recommended actions and bulleted triage steps. Use the MITRE ATT&CK data provided to add more context and recommendations from MITRE, and hyperlink to the relevant pages on MITRE\'s website. Be sure to include the user and host risk score data from the context. Your response should include steps that point to Elastic Security specific features, including endpoint response actions, the Elastic Agent OSQuery manager integration (with example osquery queries), timelines and entity analytics and link to all the relevant Elastic Security documentation.`, - } -); - -export const EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N = `${THEN_SUMMARIZE_SUGGESTED_KQL_AND_EQL_QUERIES} -${FINALLY_SUGGEST_INVESTIGATION_GUIDE_AND_FORMAT_AS_MARKDOWN}`; diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx index 64e268c54a44c..3055752b3850b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx @@ -13,7 +13,7 @@ import { bulkUpdatePrompts, } from '@kbn/elastic-assistant'; -import { once } from 'lodash/fp'; +import { once, isEmpty } from 'lodash/fp'; import type { HttpSetup } from '@kbn/core-http-browser'; import useObservable from 'react-use/lib/useObservable'; import { useKibana } from '../common/lib/kibana'; @@ -21,6 +21,7 @@ import { useKibana } from '../common/lib/kibana'; import { BASE_SECURITY_QUICK_PROMPTS } from './content/quick_prompts'; import { useAssistantAvailability } from './use_assistant_availability'; import { licenseService } from '../common/hooks/use_license'; +import { useFindPromptContexts } from './content/prompt_contexts/use_find_prompt_contexts'; import { CommentActionsPortal } from './comment_actions/comment_actions_portal'; import { AugmentMessageCodeBlocksPortal } from './use_augment_message_code_blocks/augment_message_code_blocks_portal'; import { useElasticAssistantSharedStateSignalIndex } from './use_elastic_assistant_shared_state_signal_index/use_elastic_assistant_shared_state_signal_index'; @@ -86,6 +87,30 @@ export const AssistantProvider: FC> = ({ children }) notifications, ]); + const PROMPT_CONTEXTS = useFindPromptContexts({ + context: { + isAssistantEnabled: + hasEnterpriseLicence && + assistantAvailability.isAssistantEnabled && + assistantAvailability.hasAssistantPrivilege, + httpFetch: http.fetch, + toasts: notifications.toasts, + }, + params: { + prompt_group_id: 'aiAssistant', + prompt_ids: ['alertEvaluation', 'dataQualityAnalysis', 'ruleAnalysis'], + }, + }); + const promptContext = useObservable( + elasticAssistantSharedState.promptContexts.getPromptContext$(), + {} + ); + useEffect(() => { + if (isEmpty(promptContext)) { + elasticAssistantSharedState.promptContexts.setPromptContext(PROMPT_CONTEXTS); + } + }, [elasticAssistantSharedState.promptContexts, promptContext, PROMPT_CONTEXTS]); + if (!assistantContextValue) { return null; } 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 ef9cb85c71301..819b6e8bf57c1 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 @@ -1558,10 +1558,17 @@ export const RULE_MANAGEMENT_CONTEXT_DESCRIPTION = i18n.translate( ); export const EXPLAIN_THEN_SUMMARIZE_RULE_DETAILS = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleManagement.explainThenSummarizeRuleDetails', + 'xpack.securitySolution.detectionEngine.ruleManagement.explainThenSummarizeRuleDetailsV2', { defaultMessage: - "Please explain the selected rules above. For each rule, highlight why they are relevant, the query as published on Elastic's detection rules repository and an in-depth explanation of it, and what they typically mean for an organization if detected.", + 'Please provide a comprehensive analysis of each selected Elastic Security detection rule. For each rule, include:\n' + + '- The rule name and a brief summary of its purpose.\n' + + '- The full detection query as published in Elastic’s official detection rules repository.\n' + + '- An in-depth explanation of how the query works, including key fields, logic, and detection techniques.\n' + + '- The relevance of the rule to modern threats or attack techniques (e.g., MITRE ATT&CK mapping).\n' + + '- Typical implications and recommended response actions for an organization if this rule triggers.\n' + + '- Any notable false positive considerations or tuning recommendations.\n' + + 'Format your response using markdown with clear headers for each rule, code blocks for queries, and concise bullet points for explanations.', } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx index 9b9d2d59fb6ae..66f16c9280923 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx @@ -10,7 +10,7 @@ import { renderHook } from '@testing-library/react'; import type { UseAssistantParams, UseAssistantResult } from './use_assistant'; import { useAssistant } from './use_assistant'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; -import { useAssistantOverlay } from '@kbn/elastic-assistant'; +import { useAssistantContext, useAssistantOverlay } from '@kbn/elastic-assistant'; import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; jest.mock('../../../../assistant/use_assistant_availability'); @@ -23,11 +23,11 @@ const renderUseAssistant = () => renderHook((props: UseAssistantParams) => useAssistant(props), { initialProps: { dataFormattedForFieldBrowser, isAlert }, }); +const useAssistantOverlayMock = useAssistantOverlay as jest.Mock; describe('useAssistant', () => { - let hookResult: RenderHookResult; - - it(`should return showAssistant true and a value for promptContextId`, () => { + beforeEach(() => { + jest.clearAllMocks(); jest.mocked(useAssistantAvailability).mockReturnValue({ hasSearchAILakeConfigurations: false, hasAssistantPrivilege: true, @@ -37,12 +37,44 @@ describe('useAssistant', () => { hasManageGlobalKnowledgeBase: true, isAssistantEnabled: true, }); - jest - .mocked(useAssistantOverlay) - .mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' }); + useAssistantOverlayMock.mockReturnValue({ + showAssistantOverlay: jest.fn, + promptContextId: '123', + }); - hookResult = renderUseAssistant(); + (useAssistantContext as jest.Mock).mockReturnValue({ + basePromptContexts: [ + { + category: 'alert', + description: 'Alert (from view)', + suggestedUserPrompt: 'ALERT EVALUATION', + tooltip: 'Add this alert as context', + }, + { + category: 'data-quality-dashboard', + description: 'Data Quality (index)', + suggestedUserPrompt: 'DATA QUALITY ANALYSIS', + tooltip: 'Add this Data Quality report as context', + }, + { + category: 'detection-rules', + description: 'Selected Detection Rules', + suggestedUserPrompt: 'RULE ANALYSIS', + tooltip: 'Add this alert as context', + }, + { + category: 'event', + description: 'Event (from view)', + suggestedUserPrompt: 'EVENT EVALUATION', + tooltip: 'Add this event as context', + }, + ], + }); + }); + let hookResult: RenderHookResult; + it(`should return showAssistant true and a value for promptContextId`, () => { + hookResult = renderUseAssistant(); expect(hookResult.result.current.showAssistant).toEqual(true); expect(hookResult.result.current.promptContextId).toEqual('123'); }); @@ -57,9 +89,6 @@ describe('useAssistant', () => { hasManageGlobalKnowledgeBase: true, isAssistantEnabled: true, }); - jest - .mocked(useAssistantOverlay) - .mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' }); hookResult = renderUseAssistant(); @@ -68,19 +97,6 @@ describe('useAssistant', () => { }); it('returns anonymized prompt context data', async () => { - jest.mocked(useAssistantAvailability).mockReturnValue({ - hasSearchAILakeConfigurations: false, - hasAssistantPrivilege: true, - hasConnectorsAllPrivilege: true, - hasConnectorsReadPrivilege: true, - hasUpdateAIAssistantAnonymization: true, - hasManageGlobalKnowledgeBase: true, - isAssistantEnabled: true, - }); - jest - .mocked(useAssistantOverlay) - .mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' }); - hookResult = renderUseAssistant(); const getPromptContext = (useAssistantOverlay as jest.Mock).mock.calls[0][3]; @@ -105,4 +121,16 @@ describe('useAssistant', () => { 'user.name': ['user-name'], }); }); + it('returns correct prompt for alert', () => { + renderUseAssistant(); + expect(useAssistantOverlayMock.mock.calls[0][0]).toEqual('alert'); + expect(useAssistantOverlayMock.mock.calls[0][5]).toEqual('ALERT EVALUATION'); + }); + it('returns correct prompt for event', () => { + renderHook((props: UseAssistantParams) => useAssistant(props), { + initialProps: { dataFormattedForFieldBrowser, isAlert: false }, + }); + expect(useAssistantOverlayMock.mock.calls[0][0]).toEqual('event'); + expect(useAssistantOverlayMock.mock.calls[0][5]).toEqual('EVENT EVALUATION'); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts index 4059633c9be61..7a3c137a396f9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts @@ -6,7 +6,7 @@ */ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; -import { useAssistantOverlay } from '@kbn/elastic-assistant'; +import { useAssistantContext, useAssistantOverlay } from '@kbn/elastic-assistant'; import { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; @@ -22,7 +22,6 @@ import { import { PROMPT_CONTEXT_ALERT_CATEGORY, PROMPT_CONTEXT_EVENT_CATEGORY, - PROMPT_CONTEXTS, } from '../../../../assistant/content/prompt_contexts'; const SUMMARY_VIEW = i18n.translate('xpack.securitySolution.eventDetails.summaryView', { @@ -68,6 +67,15 @@ export const useAssistant = ({ isAlert, }: UseAssistantParams): UseAssistantResult => { const { hasAssistantPrivilege, isAssistantEnabled } = useAssistantAvailability(); + const { basePromptContexts } = useAssistantContext(); + const suggestedUserPrompt = useMemo( + () => + basePromptContexts.find( + ({ category }) => + category === (isAlert ? PROMPT_CONTEXT_ALERT_CATEGORY : PROMPT_CONTEXT_EVENT_CATEGORY) + )?.suggestedUserPrompt, + [basePromptContexts, isAlert] + ); const useAssistantHook = hasAssistantPrivilege ? useAssistantOverlay : useAssistantNoop; const getPromptContext = useCallback( async () => getRawData(dataFormattedForFieldBrowser ?? []), @@ -93,9 +101,7 @@ export const useAssistant = ({ : EVENT_SUMMARY_CONTEXT_DESCRIPTION(SUMMARY_VIEW), getPromptContext, null, - isAlert - ? PROMPT_CONTEXTS[PROMPT_CONTEXT_ALERT_CATEGORY].suggestedUserPrompt - : PROMPT_CONTEXTS[PROMPT_CONTEXT_EVENT_CATEGORY].suggestedUserPrompt, + suggestedUserPrompt, isAlert ? ALERT_SUMMARY_VIEW_CONTEXT_TOOLTIP : EVENT_SUMMARY_VIEW_CONTEXT_TOOLTIP, isAssistantEnabled ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/plugin.tsx b/x-pack/solutions/security/plugins/security_solution/public/plugin.tsx index d66dfc772594f..a9eb5e1d5cfb9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/plugin.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/plugin.tsx @@ -65,7 +65,6 @@ import { PluginServices } from './plugin_services'; import { getExternalReferenceAttachmentEndpointRegular } from './cases/attachments/external_reference'; import { isSecuritySolutionAccessible } from './helpers_access'; import { generateAttachmentType } from './threat_intelligence/modules/cases/utils/attachments'; -import { PROMPT_CONTEXTS } from './assistant/content/prompt_contexts'; export class Plugin implements IPlugin { private config: SecuritySolutionUiConfigType; @@ -137,9 +136,6 @@ export class Plugin implements IPlugin { unmountApp(); - unmountPromptContext(); }; }, }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts index cbe75013979b2..19a80571a089a 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/conversations.cy.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - EXPLAIN_THEN_SUMMARIZE_RULE_DETAILS, - RULE_MANAGEMENT_CONTEXT_DESCRIPTION, -} from '@kbn/security-solution-plugin/public/detection_engine/common/translations'; -import { EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N } from '@kbn/security-solution-plugin/public/assistant/content/prompts/user/translations'; +import { RULE_MANAGEMENT_CONTEXT_DESCRIPTION } from '@kbn/security-solution-plugin/public/detection_engine/common/translations'; import { IS_SERVERLESS } from '../../env_var_names_constants'; import { assertConnectorSelected, @@ -94,7 +90,6 @@ describe('AI Assistant Conversations', { tags: ['@ess', '@serverless'] }, () => openAssistant('rule'); assertNewConversation(false, `Detection Rules - Rule 1`); assertConnectorSelected(azureConnectorAPIPayload.name); - cy.get(USER_PROMPT).should('have.text', EXPLAIN_THEN_SUMMARIZE_RULE_DETAILS); cy.get(PROMPT_CONTEXT_BUTTON(0)).should('have.text', RULE_MANAGEMENT_CONTEXT_DESCRIPTION); }); }); @@ -106,10 +101,6 @@ describe('AI Assistant Conversations', { tags: ['@ess', '@serverless'] }, () => openAssistant('alert'); assertConversationTitleContains('New Rule Test'); assertConnectorSelected(azureConnectorAPIPayload.name); - cy.get(USER_PROMPT).should( - 'have.text', - EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N - ); cy.get(PROMPT_CONTEXT_BUTTON(0)).should('have.text', 'Alert (from summary)'); }); it('Shows empty connector callout when a conversation that had a connector no longer does', () => { @@ -138,7 +129,6 @@ describe('AI Assistant Conversations', { tags: ['@ess', '@serverless'] }, () => assertConversationTitleContains('New Rule Test'); // send message to ensure conversation is created submitMessage(); - assertMessageSent(EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N); closeAssistant(); visitGetStartedPage(); openAssistant(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/prompts.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/prompts.cy.ts index a3420e346e889..94b20dfc5a1b3 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/prompts.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/ai_assistant/prompts.cy.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N } from '@kbn/security-solution-plugin/public/assistant/content/prompts/user/translations'; import { PromptCreateProps } from '@kbn/elastic-assistant-common/impl/schemas'; import { IS_SERVERLESS } from '../../env_var_names_constants'; import { QUICK_PROMPT_BADGE, USER_PROMPT } from '../../screens/ai_assistant'; @@ -182,10 +181,6 @@ describe('AI Assistant Prompts', { tags: ['@ess', '@serverless'] }, () => { expandFirstAlert(); openAssistant('alert'); cy.get(QUICK_PROMPT_BADGE(testPrompt.name)).should('be.visible'); - cy.get(USER_PROMPT).should( - 'have.text', - EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N - ); cy.get(QUICK_PROMPT_BADGE(testPrompt.name)).click(); cy.get(USER_PROMPT).should('have.text', testPrompt.content); }); From 053caa765a95fc691fbda50a20c4c49dd493a66b Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 Jun 2025 21:10:21 -0600 Subject: [PATCH 17/18] change feature flag --- .../impl/assistant_context/types.tsx | 2 -- .../packages/shared/kbn-elastic-assistant/index.ts | 2 ++ .../assistant_availability/use_assistant_availability.ts | 9 ++++++++- .../security_solution/common/experimental_features.ts | 4 ---- .../assistant/use_assistant_availability/index.tsx | 6 ------ 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx index ce024d7d02178..3c8ecbb2a70dc 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/types.tsx @@ -74,8 +74,6 @@ export interface AssistantAvailability { hasUpdateAIAssistantAnonymization: boolean; // When true, user has `Edit` privilege for `Global Knowledge Base` hasManageGlobalKnowledgeBase: boolean; - // remove once product has signed off on prompt text - isStarterPromptsEnabled: boolean; } export type GetAssistantMessages = (commentArgs: { diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/index.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/index.ts index 569ed723f2995..540c506a98cdf 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/index.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/index.ts @@ -201,4 +201,6 @@ export interface UseAssistantAvailability { hasUpdateAIAssistantAnonymization: boolean; // When true, user has `Edit` privilege for `Global Knowledge Base` hasManageGlobalKnowledgeBase: boolean; + // remove once product has signed off on prompt text + isStarterPromptsEnabled: boolean; } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts index 54a99d76f5902..9c78ca410ef8c 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts @@ -11,9 +11,13 @@ import { useKibana } from '../../context/typed_kibana_context/typed_kibana_conte import { useLicense } from '../licence/use_licence'; +export const STARTER_PROMPTS_FEATURE_FLAG = 'elasticAssistant.starterPromptsEnabled' as const; export const useAssistantAvailability = (): UseAssistantAvailability => { const isEnterprise = useLicense().isEnterprise(); - const capabilities = useKibana().services.application.capabilities; + const { + application: { capabilities }, + featureFlags, + } = useKibana().services; const hasAssistantPrivilege = capabilities[ASSISTANT_FEATURE_ID]?.['ai-assistant'] === true; const hasUpdateAIAssistantAnonymization = @@ -32,11 +36,14 @@ export const useAssistantAvailability = (): UseAssistantAvailability => { capabilities.actions?.delete === true && capabilities.actions?.save === true; + const isStarterPromptsEnabled = featureFlags.getBooleanValue(STARTER_PROMPTS_FEATURE_FLAG, false); + return { hasSearchAILakeConfigurations, hasAssistantPrivilege, hasConnectorsAllPrivilege, hasConnectorsReadPrivilege, + isStarterPromptsEnabled, isAssistantEnabled: isEnterprise, hasUpdateAIAssistantAnonymization, hasManageGlobalKnowledgeBase, 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 51f3085fa30aa..df7183cf90c33 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 @@ -279,10 +279,6 @@ export const allowedExperimentalValues = Object.freeze({ * Enables advanced mode for Trusted Apps creation and update */ trustedAppsAdvancedMode: false, - /** - * Enables the starter prompts in the Elastic Assistant - */ - starterPromptsEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx index a88850db20948..7d7ca3fabf96e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx @@ -6,7 +6,6 @@ */ import type { AssistantAvailability } from '@kbn/elastic-assistant'; -import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { useLicense } from '../../common/hooks/use_license'; import { useKibana } from '../../common/lib/kibana'; import { ASSISTANT_FEATURE_ID, SECURITY_FEATURE_ID } from '../../../common/constants'; @@ -31,17 +30,12 @@ export const useAssistantAvailability = (): AssistantAvailability => { capabilities.actions?.delete === true && capabilities.actions?.save === true; - const starterPromptsEnabled = useIsExperimentalFeatureEnabled('starterPromptsEnabled'); - // remove once product has signed off on prompt text - const isStarterPromptsEnabled = starterPromptsEnabled; - return { hasSearchAILakeConfigurations, hasAssistantPrivilege, hasConnectorsAllPrivilege, hasConnectorsReadPrivilege, isAssistantEnabled: isEnterprise, - isStarterPromptsEnabled, hasUpdateAIAssistantAnonymization, hasManageGlobalKnowledgeBase, }; From feee4001368c67105fc04bfb5b7cad65158245b7 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 24 Jun 2025 21:13:25 -0600 Subject: [PATCH 18/18] rm ff value from security_solution --- .../data_quality_panel/mock/test_providers/test_providers.tsx | 1 - .../public/common/mock/mock_assistant_provider.tsx | 1 - .../flyout/document_details/right/hooks/use_assistant.test.tsx | 2 -- 3 files changed, 4 deletions(-) diff --git a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx index e9d5c049f68ad..b4a635ebc4bcf 100644 --- a/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx +++ b/x-pack/solutions/security/packages/ecs-data-quality-dashboard/impl/data_quality_panel/mock/test_providers/test_providers.tsx @@ -59,7 +59,6 @@ const TestExternalProvidersComponent: React.FC = ({ hasUpdateAIAssistantAnonymization: true, hasManageGlobalKnowledgeBase: true, isAssistantEnabled: true, - isStarterPromptsEnabled: true, }; const queryClient = new QueryClient({ defaultOptions: { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx index 0b19233080c46..3b72222a2475f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx @@ -40,7 +40,6 @@ export const MockAssistantProviderComponent: React.FC = ({ hasUpdateAIAssistantAnonymization: true, hasManageGlobalKnowledgeBase: true, isAssistantEnabled: true, - isStarterPromptsEnabled: true, }; const chrome = chromeServiceMock.createStartContract(); chrome.getChromeStyle$.mockReturnValue(of('classic')); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx index eac7230c2dad7..66f16c9280923 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx @@ -36,7 +36,6 @@ describe('useAssistant', () => { hasUpdateAIAssistantAnonymization: true, hasManageGlobalKnowledgeBase: true, isAssistantEnabled: true, - isStarterPromptsEnabled: true, }); useAssistantOverlayMock.mockReturnValue({ showAssistantOverlay: jest.fn, @@ -89,7 +88,6 @@ describe('useAssistant', () => { hasUpdateAIAssistantAnonymization: true, hasManageGlobalKnowledgeBase: true, isAssistantEnabled: true, - isStarterPromptsEnabled: true, }); hookResult = renderUseAssistant();