Skip to content

Commit

Permalink
[Security Solution] [GenAI] [Detections] Ask security assistant to he…
Browse files Browse the repository at this point in the history
…lp 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="https://github.com/elastic/kibana/assets/915763/9f31fad5-f1e5-46b2-accf-2739ac3b83dd">

Response from security assistant:
<img width="1454" alt="threshold_rule_exception_assistant_resolved"
src="https://github.com/elastic/kibana/assets/915763/5fbd8ea5-8a5d-47ea-8f24-6698b298f023">


Available for warnings too:
<img width="1205" alt="assistant_error_help_warning"
src="https://github.com/elastic/kibana/assets/915763/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="https://github.com/elastic/kibana/assets/915763/d6e797c8-e014-4cb0-be95-fcce02568121">

---------

Co-authored-by: Garrett Spong <[email protected]>
  • Loading branch information
dhurley14 and spong authored Nov 16, 2023
1 parent 9e087f4 commit 18d65c4
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 8 deletions.
7 changes: 5 additions & 2 deletions x-pack/packages/kbn-elastic-assistant/impl/new_chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,19 +404,35 @@ 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>
<EuiLoadingSpinner size="m" data-test-subj="rule-status-loader" />
</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 }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,61 @@ 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');

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);
Expand All @@ -48,15 +92,15 @@ 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');
result.getByText(MESSAGE);
});

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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);

0 comments on commit 18d65c4

Please sign in to comment.