[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:
<img width="1462" alt="threshold_rule_exception_error"
src="9f31fad5-f1e5-46b2-accf-2739ac3b83dd">

Response from security assistant:
<img width="1454" alt="threshold_rule_exception_assistant_resolved"
src="5fbd8ea5-8a5d-47ea-8f24-6698b298f023">


Available for warnings too:
<img width="1205" alt="assistant_error_help_warning"
src="e93bb870-9688-4d87-a6db-59a552ab9af9">

Includes the rule name and data sources for pre-built rules for
additional information to generate a slightly more helpful response:

<img width="1958" alt="pre_built_rule_name_data_source"
src="d6e797c8-e014-4cb0-be95-fcce02568121">

---------

Co-authored-by: Garrett Spong <garrett.spong@elastic.co>
This commit is contained in:
Devin W. Hurley 2023-11-16 17:11:40 -05:00 committed by GitHub
parent 9e087f4580
commit 18d65c4b23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 129 additions and 8 deletions

View file

@ -21,10 +21,13 @@ export type Props = Omit<PromptContext, 'id'> & {
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<Props> = ({
category,
color = 'primary',
children = i18n.NEW_CHAT,
conversationId,
description,
@ -58,11 +61,11 @@ const NewChatComponent: React.FC<Props> = ({
return useMemo(
() => (
<EuiButtonEmpty data-test-subj="newChat" onClick={showOverlay} iconType={icon}>
<EuiButtonEmpty color={color} data-test-subj="newChat" onClick={showOverlay} iconType={icon}>
{children}
</EuiButtonEmpty>
),
[children, icon, showOverlay]
[children, icon, showOverlay, color]
);
};

View file

@ -404,6 +404,12 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
);
}, [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 ? (
<EuiFlexItem>
@ -411,12 +417,22 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
</EuiFlexItem>
) : (
<RuleStatusFailedCallOut
ruleName={rule?.immutable ? rule?.name : undefined}
dataSources={rule?.immutable ? ruleIndex : undefined}
status={lastExecutionStatus}
date={lastExecutionDate}
message={lastExecutionMessage}
/>
);
}, [lastExecutionStatus, lastExecutionDate, lastExecutionMessage, ruleLoading]);
}, [
lastExecutionStatus,
lastExecutionDate,
lastExecutionMessage,
ruleLoading,
rule?.immutable,
rule?.name,
ruleIndex,
]);
const updateDateRangeCallback = useCallback<UpdateDateRange>(
({ x }) => {

View file

@ -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 }) => (
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
augmentMessageCodeBlocks={jest.fn()}
baseAllow={[]}
baseAllowReplacement={[]}
basePath={'https://localhost:5601/kbn'}
defaultAllow={[]}
defaultAllowReplacement={[]}
docLinks={{
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
DOC_LINK_VERSION: 'current',
}}
getInitialConversations={mockGetInitialConversations}
getComments={mockGetComments}
http={mockHttp}
setConversations={jest.fn()}
setDefaultAllow={jest.fn()}
setDefaultAllowReplacement={jest.fn()}
>
{children}
</AssistantProvider>
);
describe('RuleStatusFailedCallOut', () => {
const renderWith = (status: RuleExecutionStatus | null | undefined) =>
render(<RuleStatusFailedCallOut status={status} date={DATE} message={MESSAGE} />);
const renderWithAssistant = (status: RuleExecutionStatus | null | undefined) =>
render(
<ContextWrapper>
<RuleStatusFailedCallOut status={status} date={DATE} message={MESSAGE} />{' '}
</ContextWrapper>
);
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');

View file

@ -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<RuleStatusFailedCallOutProps> = ({
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<RuleStatusFailedCallOutProps> =
>
{message}
</EuiCodeBlock>
{hasAssistantPrivilege && (
<EuiButton color={color} size="s">
<NewChat
category="detection-rules"
color={color}
conversationId={i18nAssistant.DETECTION_RULES_CONVERSATION_ID}
description={i18n.ASK_ASSISTANT_DESCRIPTION}
getPromptContext={getPromptContext}
suggestedUserPrompt={i18n.ASK_ASSISTANT_USER_PROMPT}
tooltip={i18n.ASK_ASSISTANT_TOOLTIP}
>
{i18n.ASK_ASSISTANT_ERROR_BUTTON}
</NewChat>
</EuiButton>
)}
</EuiCallOut>
</div>
);

View file

@ -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',
}
);