[8.11] [Security Solution] [Elastic AI Assistant] Fixes ES|QL codeblocks from not being able to be sent to Timeline (#169478) (#169594)

# Backport

This will backport the following commits from `main` to `8.11`:
- [[Security Solution] [Elastic AI Assistant] Fixes `ES|QL` codeblocks
from not being able to be sent to Timeline
(#169478)](https://github.com/elastic/kibana/pull/169478)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Garrett
Spong","email":"spong@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-10-23T22:01:16Z","message":"[Security
Solution] [Elastic AI Assistant] Fixes `ES|QL` codeblocks from not being
able to be sent to Timeline (#169478)\n\n## Summary\r\n\r\nFixes `ES|QL`
codeblocks from not being able to be sent to Timeline.\r\n\r\n<p
align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"fdc3b2a0-5b4b-4584-b304-c4d24de1917c\"\r\n/>\r\n</p>
\r\n\r\n## Test instructions\r\n\r\nEither request the assistant to
generate an ESQL query or just paste\r\nthis codeblock into the
conversation to test the action directly from\r\nthe user message. Be
sure to declare the codeblock language as `esql` or\r\ninclude one of
the [string
match\r\npatterns](https://github.com/elastic/kibana/pull/169478/files#diff-f70f0b96568e024e53bfbb62adcca72051f0a2e824d4ab22664eed0e149be248R38)\r\nabove
the code block so the action can be recognized.\r\n\r\n\r\n````\r\nBelow
is an `Elasticsearch Query Language` query:\r\n\r\n```esql\r\nFROM
logs-endpoint*\r\n| WHERE event.category == \\\"process\\\"\r\n| STATS
proc_count = COUNT(process.name) BY host.name\r\n| KEEP host.name,
proc_count\r\n```\r\n````\r\n\r\n\r\nNote: The `send to timeline`
actions appear to only reliably show up\r\nwhen using the Assistant
instance within Timeline. Now that we have a\r\nmore reliable way of
attaching actions via markdown plugins/parsers, we\r\nshould refactor
this code to use that method as opposed to the code\r\nblock/dom
inspection route that is used currently. In the meantime I\r\nwill see
if there is a low-impact fix that can be made
here.","sha":"93636d9fc0899f99f27acd29990f182f0d493825","branchLabelMapping":{"^v8.12.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","Team:
SecuritySolution","Feature:Elastic AI
Assistant","v8.11.0","v8.12.0"],"number":169478,"url":"https://github.com/elastic/kibana/pull/169478","mergeCommit":{"message":"[Security
Solution] [Elastic AI Assistant] Fixes `ES|QL` codeblocks from not being
able to be sent to Timeline (#169478)\n\n## Summary\r\n\r\nFixes `ES|QL`
codeblocks from not being able to be sent to Timeline.\r\n\r\n<p
align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"fdc3b2a0-5b4b-4584-b304-c4d24de1917c\"\r\n/>\r\n</p>
\r\n\r\n## Test instructions\r\n\r\nEither request the assistant to
generate an ESQL query or just paste\r\nthis codeblock into the
conversation to test the action directly from\r\nthe user message. Be
sure to declare the codeblock language as `esql` or\r\ninclude one of
the [string
match\r\npatterns](https://github.com/elastic/kibana/pull/169478/files#diff-f70f0b96568e024e53bfbb62adcca72051f0a2e824d4ab22664eed0e149be248R38)\r\nabove
the code block so the action can be recognized.\r\n\r\n\r\n````\r\nBelow
is an `Elasticsearch Query Language` query:\r\n\r\n```esql\r\nFROM
logs-endpoint*\r\n| WHERE event.category == \\\"process\\\"\r\n| STATS
proc_count = COUNT(process.name) BY host.name\r\n| KEEP host.name,
proc_count\r\n```\r\n````\r\n\r\n\r\nNote: The `send to timeline`
actions appear to only reliably show up\r\nwhen using the Assistant
instance within Timeline. Now that we have a\r\nmore reliable way of
attaching actions via markdown plugins/parsers, we\r\nshould refactor
this code to use that method as opposed to the code\r\nblock/dom
inspection route that is used currently. In the meantime I\r\nwill see
if there is a low-impact fix that can be made
here.","sha":"93636d9fc0899f99f27acd29990f182f0d493825"}},"sourceBranch":"main","suggestedTargetBranches":["8.11"],"targetPullRequestStates":[{"branch":"8.11","label":"v8.11.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.12.0","labelRegex":"^v8.12.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/169478","number":169478,"mergeCommit":{"message":"[Security
Solution] [Elastic AI Assistant] Fixes `ES|QL` codeblocks from not being
able to be sent to Timeline (#169478)\n\n## Summary\r\n\r\nFixes `ES|QL`
codeblocks from not being able to be sent to Timeline.\r\n\r\n<p
align=\"center\">\r\n<img
width=\"500\"\r\nsrc=\"fdc3b2a0-5b4b-4584-b304-c4d24de1917c\"\r\n/>\r\n</p>
\r\n\r\n## Test instructions\r\n\r\nEither request the assistant to
generate an ESQL query or just paste\r\nthis codeblock into the
conversation to test the action directly from\r\nthe user message. Be
sure to declare the codeblock language as `esql` or\r\ninclude one of
the [string
match\r\npatterns](https://github.com/elastic/kibana/pull/169478/files#diff-f70f0b96568e024e53bfbb62adcca72051f0a2e824d4ab22664eed0e149be248R38)\r\nabove
the code block so the action can be recognized.\r\n\r\n\r\n````\r\nBelow
is an `Elasticsearch Query Language` query:\r\n\r\n```esql\r\nFROM
logs-endpoint*\r\n| WHERE event.category == \\\"process\\\"\r\n| STATS
proc_count = COUNT(process.name) BY host.name\r\n| KEEP host.name,
proc_count\r\n```\r\n````\r\n\r\n\r\nNote: The `send to timeline`
actions appear to only reliably show up\r\nwhen using the Assistant
instance within Timeline. Now that we have a\r\nmore reliable way of
attaching actions via markdown plugins/parsers, we\r\nshould refactor
this code to use that method as opposed to the code\r\nblock/dom
inspection route that is used currently. In the meantime I\r\nwill see
if there is a low-impact fix that can be made
here.","sha":"93636d9fc0899f99f27acd29990f182f0d493825"}}]}] BACKPORT-->

Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2023-10-23 19:33:17 -04:00 committed by GitHub
parent 68e8bc6328
commit 03866cb70b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 184 additions and 31 deletions

View file

@ -18,7 +18,7 @@ export interface CodeBlockDetails {
button?: React.ReactNode;
}
export type QueryType = 'eql' | 'kql' | 'dsl' | 'json' | 'no-type';
export type QueryType = 'eql' | 'esql' | 'kql' | 'dsl' | 'json' | 'no-type' | 'sql';
/**
* `analyzeMarkdown` is a helper that enriches content returned from a query
@ -35,6 +35,7 @@ export const analyzeMarkdown = (markdown: string): CodeBlockDetails[] => {
// If your codeblocks aren't getting tagged with the right language, add keywords to the array.
const types = {
eql: ['Event Query Language', 'EQL sequence query', 'EQL'],
esql: ['Elasticsearch Query Language', 'ESQL', 'ES|QL', 'SQL'],
kql: ['Kibana Query Language', 'KQL Query', 'KQL'],
dsl: [
'Elasticsearch QueryDSL',

View file

@ -66,29 +66,29 @@ const StartAppComponent: FC<StartAppComponent> = ({
<ReduxStoreProvider store={store}>
<KibanaThemeProvider theme$={theme$}>
<EuiThemeProvider darkMode={darkMode}>
<AssistantProvider>
<MlCapabilitiesProvider>
<UserPrivilegesProvider kibanaCapabilities={capabilities}>
<ManageUserInfo>
<NavigationProvider core={services}>
<ReactQueryClientProvider>
<CellActionsProvider
getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}
>
<UpsellingProvider upsellingService={upselling}>
<DiscoverInTimelineContextProvider>
<MlCapabilitiesProvider>
<UserPrivilegesProvider kibanaCapabilities={capabilities}>
<ManageUserInfo>
<NavigationProvider core={services}>
<ReactQueryClientProvider>
<CellActionsProvider
getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}
>
<UpsellingProvider upsellingService={upselling}>
<DiscoverInTimelineContextProvider>
<AssistantProvider>
<PageRouter history={history} onAppLeave={onAppLeave}>
{children}
</PageRouter>
</DiscoverInTimelineContextProvider>
</UpsellingProvider>
</CellActionsProvider>
</ReactQueryClientProvider>
</NavigationProvider>
</ManageUserInfo>
</UserPrivilegesProvider>
</MlCapabilitiesProvider>
</AssistantProvider>
</AssistantProvider>
</DiscoverInTimelineContextProvider>
</UpsellingProvider>
</CellActionsProvider>
</ReactQueryClientProvider>
</NavigationProvider>
</ManageUserInfo>
</UserPrivilegesProvider>
</MlCapabilitiesProvider>
</EuiThemeProvider>
</KibanaThemeProvider>
<ErrorToastDispatcher />

View file

@ -0,0 +1,41 @@
/*
* 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 { EuiCodeBlock, EuiFlexGroup, EuiFlexItem, EuiPanel, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
import React from 'react';
export const CustomCodeBlock = ({ value }: { value: string }) => {
const theme = useEuiTheme();
return (
<EuiPanel
hasShadow={false}
hasBorder={false}
paddingSize="s"
className={css`
background-color: ${theme.euiTheme.colors.lightestShade};
.euiCodeBlock__pre {
margin-bottom: 0;
padding: ${theme.euiTheme.size.m};
min-block-size: 48px;
}
.euiCodeBlock__controls {
inset-block-start: ${theme.euiTheme.size.m};
inset-inline-end: ${theme.euiTheme.size.m};
}
`}
>
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiCodeBlock isCopyable fontSize="m">
{value}
</EuiCodeBlock>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -0,0 +1,35 @@
/*
* 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 type { Node } from 'unist';
import type { Parent } from 'mdast';
export const customCodeBlockLanguagePlugin = () => {
const visitor = (node: Node, parent?: Parent) => {
if ('children' in node) {
const nodeAsParent = node as Parent;
nodeAsParent.children.forEach((child) => {
visitor(child, nodeAsParent);
});
}
if (
node.type === 'code' &&
(node.lang === 'eql' ||
node.lang === 'esql' ||
node.lang === 'kql' ||
node.lang === 'dsl' ||
node.lang === 'json')
) {
node.type = 'customCodeBlock';
}
};
return (tree: Node) => {
visitor(tree);
};
};

View file

@ -7,7 +7,15 @@
import type { EuiCommentProps } from '@elastic/eui';
import type { Conversation } from '@kbn/elastic-assistant';
import { EuiAvatar, EuiMarkdownFormat, EuiText, tint } from '@elastic/eui';
import {
EuiAvatar,
EuiMarkdownFormat,
EuiSpacer,
EuiText,
getDefaultEuiMarkdownParsingPlugins,
getDefaultEuiMarkdownProcessingPlugins,
tint,
} from '@elastic/eui';
import React from 'react';
import { AssistantAvatar } from '@kbn/elastic-assistant';
@ -15,6 +23,8 @@ import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { CommentActions } from '../comment_actions';
import * as i18n from './translations';
import { customCodeBlockLanguagePlugin } from './custom_codeblock/custom_codeblock_markdown_plugin';
import { CustomCodeBlock } from './custom_codeblock/custom_code_block';
export const getComments = ({
currentConversation,
@ -24,8 +34,29 @@ export const getComments = ({
currentConversation: Conversation;
lastCommentRef: React.MutableRefObject<HTMLDivElement | null>;
showAnonymizedValues: boolean;
}): EuiCommentProps[] =>
currentConversation.messages.map((message, index) => {
}): EuiCommentProps[] => {
const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
const processingPlugins = getDefaultEuiMarkdownProcessingPlugins();
const { components } = processingPlugins[1][1];
processingPlugins[1][1].components = {
...components,
customCodeBlock: (props) => {
return (
<>
<CustomCodeBlock value={props.value} />
<EuiSpacer size="m" />
</>
);
},
};
// Fun fact: must spread existing parsingPlugins last
const parsingPluginList = [customCodeBlockLanguagePlugin, ...parsingPlugins];
const processingPluginList = processingPlugins;
return currentConversation.messages.map((message, index) => {
const isUser = message.role === 'user';
const replacements = currentConversation.replacements;
const messageContentWithReplacements =
@ -45,13 +76,21 @@ export const getComments = ({
children:
index !== currentConversation.messages.length - 1 ? (
<EuiText>
<EuiMarkdownFormat className={`message-${index}`}>
<EuiMarkdownFormat
className={`message-${index}`}
parsingPluginList={parsingPluginList}
processingPluginList={processingPluginList}
>
{showAnonymizedValues ? message.content : transformedMessage.content}
</EuiMarkdownFormat>
</EuiText>
) : (
<EuiText>
<EuiMarkdownFormat className={`message-${index}`}>
<EuiMarkdownFormat
className={`message-${index}`}
parsingPluginList={parsingPluginList}
processingPluginList={processingPluginList}
>
{showAnonymizedValues ? message.content : transformedMessage.content}
</EuiMarkdownFormat>
<span ref={lastCommentRef} />
@ -82,3 +121,4 @@ export const getComments = ({
: {}),
};
});
};

View file

@ -49,7 +49,13 @@ export const getPromptContextFromEventDetailsItem = (data: TimelineEventsDetails
return getFieldsAsCsv(allFields);
};
const sendToTimelineEligibleQueryTypes: Array<CodeBlockDetails['type']> = ['kql', 'dsl', 'eql'];
const sendToTimelineEligibleQueryTypes: Array<CodeBlockDetails['type']> = [
'kql',
'dsl',
'eql',
'esql',
'sql', // Models often put the code block language as sql, for esql, so adding this as a fallback
];
/**
* Returns message contents with replacements applied.

View file

@ -26,9 +26,11 @@ import {
applyKqlFilterQuery,
setActiveTabTimeline,
setFilters,
showTimeline,
updateDataView,
updateEqlOptions,
} from '../../timelines/store/timeline/actions';
import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context';
export interface SendToTimelineButtonProps {
asEmptyButton: boolean;
@ -50,6 +52,8 @@ export const SendToTimelineButton: React.FunctionComponent<SendToTimelineButtonP
}) => {
const dispatch = useDispatch();
const { discoverStateContainer } = useDiscoverInTimelineContext();
const getDataViewsSelector = useMemo(
() => sourcererSelectors.getSourcererDataViewsSelector(),
[]
@ -68,6 +72,30 @@ export const SendToTimelineButton: React.FunctionComponent<SendToTimelineButtonP
const configureAndOpenTimeline = useCallback(() => {
if (dataProviders || filters) {
// If esql, don't reset filters or mess with dataview & time range
if (dataProviders?.[0]?.queryType === 'esql' || dataProviders?.[0]?.queryType === 'sql') {
discoverStateContainer.current?.appState.update({
query: {
query: dataProviders[0].kqlQuery,
language: 'esql',
},
});
dispatch(
setActiveTabTimeline({
id: TimelineId.active,
activeTab: TimelineTabs.esql,
})
);
dispatch(
showTimeline({
id: TimelineId.active,
show: true,
})
);
return;
}
// Reset the current timeline
if (timeRange) {
clearTimeline({
@ -147,6 +175,7 @@ export const SendToTimelineButton: React.FunctionComponent<SendToTimelineButtonP
break;
}
}
// Use filters if more than a certain amount of ids for dom performance.
if (filters) {
dispatch(
@ -172,13 +201,14 @@ export const SendToTimelineButton: React.FunctionComponent<SendToTimelineButtonP
}
}, [
dataProviders,
clearTimeline,
dispatch,
defaultDataView.id,
signalIndexName,
filters,
timeRange,
keepDataView,
dispatch,
clearTimeline,
discoverStateContainer,
defaultDataView.id,
signalIndexName,
]);
return asEmptyButton ? (