Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Detection Engine] adds AI Assistant to rule create form #179091

Merged
merged 56 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
7bfbcbb
[Security Solution][Detection Engine] adds AI Assistant to rule creat…
vitaliidm Mar 20, 2024
ac2a20f
adjust assistant for the rest of queries
vitaliidm Mar 21, 2024
8c16a97
tune ES|QL validation error
vitaliidm Mar 21, 2024
4c9e26d
fix formatting
vitaliidm Mar 21, 2024
3626023
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Apr 22, 2024
ee1c0cb
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 3, 2024
5f82c6b
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 10, 2024
496a7d4
remove unused translations
vitaliidm May 10, 2024
990d9b3
fix cypress test
vitaliidm May 10, 2024
49a7a8d
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 10, 2024
162b268
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 13, 2024
af60f98
show AI assistant only when query is invalid
vitaliidm May 13, 2024
c99a98b
Merge branch 'de_on_week/esql-ai-assistant' of https://github.com/vit…
vitaliidm May 13, 2024
07774cc
WIP assistant v1
vitaliidm May 14, 2024
3b6f0f7
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 14, 2024
dc13ca1
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 15, 2024
3874c80
refactoring
vitaliidm May 15, 2024
86bf7ac
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 15, 2024
09a8a95
refactoring
vitaliidm May 15, 2024
bd6eee6
Merge branch 'de_on_week/esql-ai-assistant' of https://github.com/vit…
vitaliidm May 15, 2024
02a7351
add tests
vitaliidm May 15, 2024
82274c1
add more tests
vitaliidm May 15, 2024
0e868aa
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 16, 2024
43f4fb1
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 16, 2024
cf200d1
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 17, 2024
323e1a3
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 20, 2024
469e4d7
fix typings after merge conflicts resolve
vitaliidm May 20, 2024
672a58e
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 28, 2024
91739fc
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm May 30, 2024
c07ed57
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 3, 2024
46b03bc
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 4, 2024
9d23eb1
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 4, 2024
be86744
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 11, 2024
12d16cb
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 13, 2024
6e27b70
Merge branch 'main' into de_on_week/esql-ai-assistant
kibanamachine Jun 18, 2024
70f66c9
Merge branch 'main' into de_on_week/esql-ai-assistant
kibanamachine Jun 18, 2024
29dab89
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 18, 2024
b5d4156
CR feedback
vitaliidm Jun 19, 2024
8801a3a
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 19, 2024
971c2d2
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 24, 2024
6b7f8dd
add Featuire Flag
vitaliidm Jun 24, 2024
a8c2433
Merge branch 'de_on_week/esql-ai-assistant' of https://github.com/vit…
vitaliidm Jun 24, 2024
4d94465
enable for CI testing
vitaliidm Jun 24, 2024
6a16b79
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 26, 2024
d4a4a96
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 27, 2024
34ad029
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 28, 2024
338933c
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jun 28, 2024
be358cb
CR
vitaliidm Jun 28, 2024
0472dac
Merge branch 'de_on_week/esql-ai-assistant' of https://github.com/vit…
vitaliidm Jun 28, 2024
2a86962
redeploy
vitaliidm Jun 28, 2024
4b11f56
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jul 1, 2024
e33f11e
fix user prompt
vitaliidm Jul 1, 2024
0b205a0
Update experimental_features.ts
vitaliidm Jul 1, 2024
8b981f9
fix jest test
vitaliidm Jul 1, 2024
1d6f7f7
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jul 1, 2024
800a1e5
Merge branch 'main' into de_on_week/esql-ai-assistant
vitaliidm Jul 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions x-pack/packages/kbn-elastic-assistant/impl/new_chat/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,34 @@ describe('NewChat', () => {

expect(mockUseAssistantOverlay.showAssistantOverlay).toHaveBeenCalledWith(true);
});

it('renders new chat as link', () => {
render(<NewChat {...defaultProps} asLink={true} />);

const newChatLink = screen.getByTestId('newChatLink');

expect(newChatLink).toBeInTheDocument();
});

it('calls onShowOverlay callback on click', () => {
const onShowOverlaySpy = jest.fn();
render(<NewChat {...defaultProps} onShowOverlay={onShowOverlaySpy} />);

const newChatButton = screen.getByTestId('newChat');

userEvent.click(newChatButton);

expect(onShowOverlaySpy).toHaveBeenCalled();
});

it('calls onShowOverlay callback on click for link', () => {
const onShowOverlaySpy = jest.fn();
render(<NewChat {...defaultProps} asLink={true} onShowOverlay={onShowOverlaySpy} />);

const newChatLink = screen.getByTestId('newChatLink');

userEvent.click(newChatLink);

expect(onShowOverlaySpy).toHaveBeenCalled();
});
});
35 changes: 26 additions & 9 deletions x-pack/packages/kbn-elastic-assistant/impl/new_chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { EuiButtonEmpty } from '@elastic/eui';
import { EuiButtonEmpty, EuiLink } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';

import { PromptContext } from '../assistant/prompt_context/types';
Expand All @@ -17,14 +17,18 @@ export type Props = Omit<PromptContext, 'id'> & {
children?: React.ReactNode;
/** Optionally automatically add this context to a conversation when the assistant is shown */
conversationId?: string;
/** Defaults to `discuss`. If null, the button will not have an icon */
/** Defaults to `discuss`. If null, the button will not have an icon. Not available for link */
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';
/** Required to identify the availability of the Assistant for the current license level */
isAssistantEnabled: boolean;
/** Optionally render new chat as a link */
asLink?: boolean;
/** Optional callback when overlay shows */
onShowOverlay?: () => void;
};

const NewChatComponent: React.FC<Props> = ({
Expand All @@ -39,6 +43,8 @@ const NewChatComponent: React.FC<Props> = ({
suggestedUserPrompt,
tooltip,
isAssistantEnabled,
asLink = false,
onShowOverlay,
}) => {
const { showAssistantOverlay } = useAssistantOverlay(
category,
Expand All @@ -53,7 +59,8 @@ const NewChatComponent: React.FC<Props> = ({

const showOverlay = useCallback(() => {
showAssistantOverlay(true);
}, [showAssistantOverlay]);
onShowOverlay?.();
}, [showAssistantOverlay, onShowOverlay]);

const icon = useMemo(() => {
if (iconType === null) {
Expand All @@ -64,12 +71,22 @@ const NewChatComponent: React.FC<Props> = ({
}, [iconType]);

return useMemo(
() => (
<EuiButtonEmpty color={color} data-test-subj="newChat" onClick={showOverlay} iconType={icon}>
{children}
</EuiButtonEmpty>
),
[children, icon, showOverlay, color]
() =>
asLink ? (
<EuiLink color={color} data-test-subj="newChatLink" onClick={showOverlay}>
{children}
</EuiLink>
) : (
<EuiButtonEmpty
color={color}
data-test-subj="newChat"
onClick={showOverlay}
iconType={icon}
>
{children}
</EuiButtonEmpty>
),
[children, icon, showOverlay, color, asLink]
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ export const allowedExperimentalValues = Object.freeze({
*/
protectionUpdatesEnabled: true,

/**
* Enables AI assistant on rule creation form when query has error
*/
AIAssistantOnRuleCreationFormEnabled: false,

/**
* Disables the timeline save tour.
* This flag is used to disable the tour in cypress tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export enum TELEMETRY_EVENT {
ADD_INVESTIGATION_FIELDS = 'add_investigation_fields',
SET_INVESTIGATION_FIELDS = 'set_investigation_fields',
DELETE_INVESTIGATION_FIELDS = 'delete_investigation_fields',

// AI assistant on rule creation form
OPEN_ASSISTANT_ON_RULE_QUERY_ERROR = 'open_assistant_on_rule_query_error',
}

export enum TelemetryEventTypes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const esqlValidator = async (
if (isMissingMetadataOperator) {
return {
code: ERROR_CODES.ERR_MISSING_ID_FIELD_FROM_RESULT,
message: i18n.ESQL_VALIDATION_MISSING_ID_IN_QUERY_ERROR,
message: i18n.ESQL_VALIDATION_MISSING_METADATA_OPERATOR_IN_QUERY_ERROR,
};
}

Expand All @@ -84,7 +84,7 @@ export const esqlValidator = async (
if (!isEsqlQueryAggregating && !isIdFieldPresent) {
return {
code: ERROR_CODES.ERR_MISSING_ID_FIELD_FROM_RESULT,
message: i18n.ESQL_VALIDATION_MISSING_ID_IN_QUERY_ERROR,
message: i18n.ESQL_VALIDATION_MISSING_ID_FIELD_IN_QUERY_ERROR,
};
}
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@ export const esqlValidationErrorMessage = (message: string) =>
defaultMessage: 'Error validating ES|QL: "{message}"',
});

export const ESQL_VALIDATION_MISSING_ID_IN_QUERY_ERROR = i18n.translate(
'xpack.securitySolution.detectionEngine.esqlValidation.missingIdInQueryError',
export const ESQL_VALIDATION_MISSING_METADATA_OPERATOR_IN_QUERY_ERROR = i18n.translate(
'xpack.securitySolution.detectionEngine.esqlValidation.missingMetadataOperatorInQueryError',
{
defaultMessage: `Queries that don’t use the STATS...BY function (non-aggregating queries) must include the "metadata _id, _version, _index" operator after the source command. For example: FROM logs* [metadata _id, _version, _index].`,
}
);

export const ESQL_VALIDATION_MISSING_ID_FIELD_IN_QUERY_ERROR = i18n.translate(
'xpack.securitySolution.detectionEngine.esqlValidation.missingIdFieldInQueryError',
{
defaultMessage: `Queries that don’t use the STATS...BY function (non-aggregating queries) must include the "metadata _id, _version, _index" operator after the source command. For example: FROM logs* metadata _id, _version, _index. In addition, the metadata properties (_id, _version, and _index) must be returned in the query response.`,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 React from 'react';
import { screen, render } from '@testing-library/react';

import { TestProviders } from '../../../../common/mock';
import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability';

import { AiAssistant } from '.';

jest.mock('../../../../assistant/use_assistant_availability', () => ({
useAssistantAvailability: jest.fn(),
}));

const useAssistantAvailabilityMock = useAssistantAvailability as jest.Mock;

describe('AiAssistant', () => {
beforeEach(() => {
useAssistantAvailabilityMock.mockReturnValue({ hasAssistantPrivilege: true });
});
it('does not render chat component when does not have hasAssistantPrivilege', () => {
useAssistantAvailabilityMock.mockReturnValue({ hasAssistantPrivilege: false });

const { container } = render(<AiAssistant getFields={jest.fn()} />, {
wrapper: TestProviders,
});

expect(container).toBeEmptyDOMElement();
});
it('renders chat component when has hasAssistantPrivilege', () => {
render(<AiAssistant getFields={jest.fn()} />, {
wrapper: TestProviders,
});

expect(screen.getByTestId('newChatLink')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -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 React, { useCallback } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';

import { NewChat, AssistantAvatar } from '@kbn/elastic-assistant';

import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../../common/lib/telemetry';
import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability';
import * as i18nAssistant from '../../../../detections/pages/detection_engine/rules/translations';
import type { DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types';
import type { FormHook, ValidationError } from '../../../../shared_imports';

import * as i18n from './translations';

const getLanguageName = (language: string | undefined) => {
let modifiedLanguage = language;
if (language === 'eql') {
modifiedLanguage = 'EQL(Event Query Language)';
}
if (language === 'esql') {
modifiedLanguage = 'ES|QL(The Elasticsearch Query Language)';
}

return modifiedLanguage;
};

const retrieveErrorMessages = (errors: ValidationError[]): string =>
errors
.flatMap(({ message, messages }) => [message, ...(Array.isArray(messages) ? messages : [])])
.join(', ');

interface AiAssistantProps {
getFields: FormHook<DefineStepRule>['getFields'];
language?: string | undefined;
}

const AiAssistantComponent: React.FC<AiAssistantProps> = ({ getFields, language }) => {
const { hasAssistantPrivilege, isAssistantEnabled } = useAssistantAvailability();

const languageName = getLanguageName(language);

const getPromptContext = useCallback(async () => {
const queryField = getFields().queryBar;
const { query } = (queryField.value as DefineStepRule['queryBar']).query;

if (!query) {
return '';
}

if (queryField.errors.length === 0) {
return `No errors in ${languageName} language query detected. Current query: ${query.trim()}`;
}

return `${languageName} language query written for Elastic Security Detection rules: \"${query.trim()}\"
returns validation error on form: \"${retrieveErrorMessages(queryField.errors)}\"
Fix ${languageName} language query and give an example of it in markdown format that can be copied.
Proposed solution should be valid and must not contain new line symbols (\\n)`;
}, [getFields, languageName]);

const onShowOverlay = useCallback(() => {
track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.OPEN_ASSISTANT_ON_RULE_QUERY_ERROR);
}, []);

if (!hasAssistantPrivilege) {
return null;
}

return (
<>
<EuiSpacer size="s" />

<FormattedMessage
id="xpack.securitySolution.detectionEngine.createRule.stepDefineRule.askAssistantHelpText"
defaultMessage="{AiAssistantNewChatLink} to help resolve this error."
values={{
AiAssistantNewChatLink: (
<NewChat
asLink={true}
category="detection-rules"
conversationId={i18nAssistant.DETECTION_RULES_CONVERSATION_ID}
description={i18n.ASK_ASSISTANT_DESCRIPTION}
getPromptContext={getPromptContext}
suggestedUserPrompt={i18n.ASK_ASSISTANT_USER_PROMPT(languageName)}
tooltip={i18n.ASK_ASSISTANT_TOOLTIP}
iconType={null}
onShowOverlay={onShowOverlay}
isAssistantEnabled={isAssistantEnabled}
>
<AssistantAvatar size="xxs" /> {i18n.ASK_ASSISTANT_ERROR_BUTTON}
</NewChat>
),
}}
/>
</>
);
};

export const AiAssistant = React.memo(AiAssistantComponent);
AiAssistant.displayName = 'AiAssistant';
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 { i18n } from '@kbn/i18n';

export const ASK_ASSISTANT_ERROR_BUTTON = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.askAssistant',
{
defaultMessage: 'Ask Assistant',
}
);

export const ASK_ASSISTANT_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.askAssistantDesc',
{
defaultMessage: 'Rule query error',
}
);

export const ASK_ASSISTANT_USER_PROMPT = (language: string | undefined) =>
i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.askAssistantUserPrompt',
{
defaultMessage:
'Explain all the errors present in the {language} query above. Generate a new working query, making sure all the errors are resolved in your response.',
values: { language },
}
);

export const ASK_ASSISTANT_TOOLTIP = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.askAssistantToolTip',
{
defaultMessage: 'Fix query or generate new one',
}
);
Loading