From 18d65c4b23796a40243bceadb7a7507381636e51 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Thu, 16 Nov 2023 17:11:40 -0500 Subject: [PATCH] [Security Solution] [GenAI] [Detections] Ask security assistant to help diagnose rule execution errors (#166778) ## Summary Thanks @spong for the speedy assistance with getting this code-complete! Utilizing the Security Assistant to provide some suggested mediation steps for rule errors could help customers to better self-diagnose rule errors. Thus, enhancing their experience with the Security Solution and potentially reducing new support tickets. Error on rule details page: threshold_rule_exception_error Response from security assistant: threshold_rule_exception_assistant_resolved Available for warnings too: assistant_error_help_warning Includes the rule name and data sources for pre-built rules for additional information to generate a slightly more helpful response: pre_built_rule_name_data_source --------- Co-authored-by: Garrett Spong --- .../impl/new_chat/index.tsx | 7 ++- .../pages/rule_details/index.tsx | 18 ++++++- .../rule_status_failed_callout.test.tsx | 50 +++++++++++++++++-- .../rule_status_failed_callout.tsx | 34 ++++++++++++- .../rule_execution_status/translations.ts | 28 +++++++++++ 5 files changed, 129 insertions(+), 8 deletions(-) diff --git a/x-pack/packages/kbn-elastic-assistant/impl/new_chat/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/new_chat/index.tsx index 3f6b7afc14c71..3fd798f38c4b9 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/new_chat/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/new_chat/index.tsx @@ -21,10 +21,13 @@ export type Props = Omit & { iconType?: string | null; /** Optionally specify a well known ID, or default to a UUID */ promptContextId?: string; + /** Optionally specify color of empty button */ + color?: 'text' | 'accent' | 'primary' | 'success' | 'warning' | 'danger'; }; const NewChatComponent: React.FC = ({ category, + color = 'primary', children = i18n.NEW_CHAT, conversationId, description, @@ -58,11 +61,11 @@ const NewChatComponent: React.FC = ({ return useMemo( () => ( - + {children} ), - [children, icon, showOverlay] + [children, icon, showOverlay, color] ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index df33b31a4cb61..e8a97e974431a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -404,6 +404,12 @@ const RuleDetailsPageComponent: React.FC = ({ ); }, [ruleId, lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]); + // Extract rule index if available on rule type + let ruleIndex: string[] | undefined; + if (rule != null && 'index' in rule && Array.isArray(rule.index)) { + ruleIndex = rule.index; + } + const ruleError = useMemo(() => { return ruleLoading ? ( @@ -411,12 +417,22 @@ const RuleDetailsPageComponent: React.FC = ({ ) : ( ); - }, [lastExecutionStatus, lastExecutionDate, lastExecutionMessage, ruleLoading]); + }, [ + lastExecutionStatus, + lastExecutionDate, + lastExecutionMessage, + ruleLoading, + rule?.immutable, + rule?.name, + ruleIndex, + ]); const updateDateRangeCallback = useCallback( ({ x }) => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx index 4902ac0115fee..5998073d65cdb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx @@ -11,6 +11,10 @@ import { render } from '@testing-library/react'; import type { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring'; import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring'; import { RuleStatusFailedCallOut } from './rule_status_failed_callout'; +import { AssistantProvider } from '@kbn/elastic-assistant'; +import type { AssistantAvailability } from '@kbn/elastic-assistant'; +import { httpServiceMock } from '@kbn/core-http-browser-mocks'; +import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock'; jest.mock('../../../../common/lib/kibana'); @@ -18,10 +22,50 @@ const TEST_ID = 'ruleStatusFailedCallOut'; const DATE = '2022-01-27T15:03:31.176Z'; const MESSAGE = 'This rule is attempting to query data but...'; +const actionTypeRegistry = actionTypeRegistryMock.create(); +const mockGetInitialConversations = jest.fn(() => ({})); +const mockGetComments = jest.fn(() => []); +const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' }); +const mockAssistantAvailability: AssistantAvailability = { + hasAssistantPrivilege: false, + hasConnectorsAllPrivilege: true, + hasConnectorsReadPrivilege: true, + isAssistantEnabled: true, +}; +const ContextWrapper: React.FC = ({ children }) => ( + + {children} + +); + describe('RuleStatusFailedCallOut', () => { const renderWith = (status: RuleExecutionStatus | null | undefined) => render(); - + const renderWithAssistant = (status: RuleExecutionStatus | null | undefined) => + render( + + {' '} + + ); it('is hidden if status is undefined', () => { const result = renderWith(undefined); expect(result.queryByTestId(TEST_ID)).toBe(null); @@ -48,7 +92,7 @@ describe('RuleStatusFailedCallOut', () => { }); it('is visible if status is "partial failure"', () => { - const result = renderWith(RuleExecutionStatusEnum['partial failure']); + const result = renderWithAssistant(RuleExecutionStatusEnum['partial failure']); result.getByTestId(TEST_ID); result.getByText('Warning at'); result.getByText('Jan 27, 2022 @ 15:03:31.176'); @@ -56,7 +100,7 @@ describe('RuleStatusFailedCallOut', () => { }); it('is visible if status is "failed"', () => { - const result = renderWith(RuleExecutionStatusEnum.failed); + const result = renderWithAssistant(RuleExecutionStatusEnum.failed); result.getByTestId(TEST_ID); result.getByText('Rule failure at'); result.getByText('Jan 27, 2022 @ 15:03:31.176'); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx index b9e26dc162207..0d1777e26c553 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx @@ -5,28 +5,43 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; -import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; +import { EuiButton, EuiCallOut, EuiCodeBlock } from '@elastic/eui'; +import { NewChat } from '@kbn/elastic-assistant'; import { FormattedDate } from '../../../../common/components/formatted_date'; import type { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring'; import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring'; import * as i18n from './translations'; +import * as i18nAssistant from '../../../pages/detection_engine/rules/translations'; +import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; interface RuleStatusFailedCallOutProps { + ruleName?: string | undefined; + dataSources?: string[] | undefined; date: string; message: string; status?: RuleExecutionStatus | null; } const RuleStatusFailedCallOutComponent: React.FC = ({ + ruleName, + dataSources, date, message, status, }) => { + const { hasAssistantPrivilege } = useAssistantAvailability(); const { shouldBeDisplayed, color, title } = getPropsByStatus(status); + const getPromptContext = useCallback( + async () => + ruleName != null && dataSources != null + ? `Rule name: ${ruleName}\nData sources: ${dataSources}\nError message: ${message}` + : `Error message: ${message}`, + [message, ruleName, dataSources] + ); if (!shouldBeDisplayed) { return null; } @@ -60,6 +75,21 @@ const RuleStatusFailedCallOutComponent: React.FC = > {message} + {hasAssistantPrivilege && ( + + + {i18n.ASK_ASSISTANT_ERROR_BUTTON} + + + )} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/translations.ts index e2a1e566813a3..3b760ac68e628 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/translations.ts @@ -48,3 +48,31 @@ export const PARTIAL_FAILURE_CALLOUT_TITLE = i18n.translate( defaultMessage: 'Warning at', } ); + +export const ASK_ASSISTANT_ERROR_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleStatus.askAssistant', + { + defaultMessage: 'Ask Assistant', + } +); + +export const ASK_ASSISTANT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleStatus.askAssistantDesc', + { + defaultMessage: "Rule's execution failure message", + } +); + +export const ASK_ASSISTANT_USER_PROMPT = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleStatus.askAssistantUserPrompt', + { + defaultMessage: 'Can you explain this rule execution error and steps to fix?', + } +); + +export const ASK_ASSISTANT_TOOLTIP = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleStatus.askAssistantToolTip', + { + defaultMessage: 'Add this rule execution error as context', + } +);