mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[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:
parent
9e087f4580
commit
18d65c4b23
5 changed files with 129 additions and 8 deletions
|
@ -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]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue