Skip to content

Commit

Permalink
[Security Solution][Detection Engine] adds AI Assistant to rule creat…
Browse files Browse the repository at this point in the history
…e form (#179091)

## Summary

- adds AI assistant for queries for every rule type, apart Machine
Learning
- AI assistant is shown only when query is not empty and invalid
- When user clicks on assistant it records telemetry event
`open_assistant_on_rule_query_error `
- hidden behind `AIAssistantOnRuleCreationFormEnabled` feature flag


## Design

[Design](https://www.figma.com/file/nbgUduTmTpYNXLf1vDMP7u/General-Enhancements?type=design&node-id=115%3A5166&mode=design&t=2Yi5wvS1aDoYxuyT-1)


![AI assistant help
contextual](https://github.com/elastic/kibana/assets/119798995/c2ad0989-dd1a-4082-be83-bed7741131be)


## Demo



https://github.com/elastic/kibana/assets/92328789/92435f3b-c51e-471b-940f-604a1f245e94



## Old Demoes

**Note: old demo videos use old UI design, and assistant is shown even
for valid queries.**

<details>

<summary>list of videos</summary>

### ES|QL Case 1
Simple ES|QL query validation error solving
There 2 problems in query highlighted by validation.
First, missing metadata operator
Second, operator `=` instead of `==`
By feeding query twice in Ai Assistant, I was able to get working
solution


https://github.com/elastic/kibana/assets/92328789/1eb49505-b161-4fdb-ac3c-d2833c16e2cd

### ES|QL Case 2

Fixes missing _id field, when metadata operator is present



https://github.com/elastic/kibana/assets/92328789/82024fcb-822e-46f1-a80a-8b9f1725816e

### EQL Case 1

fixes EQL typo



https://github.com/elastic/kibana/assets/92328789/ea18ceec-92f8-4322-b359-50e689a0ef72

</details>

### Issues

Results might not be always consistent and for more complex queries they
might not correct



https://github.com/elastic/kibana/assets/92328789/e3bedfd6-943c-4979-8708-f6c33d1756a6

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
vitaliidm and kibanamachine authored Jul 1, 2024
1 parent a6cc252 commit 3506e14
Show file tree
Hide file tree
Showing 14 changed files with 334 additions and 17 deletions.
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

0 comments on commit 3506e14

Please sign in to comment.