From 843821a5f25067098d045ddc768b8774e839a313 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Wed, 19 Oct 2022 17:28:39 +0000 Subject: [PATCH 01/21] WIP starting to work --- .../markdown_editor/plugins/index.ts | 4 + .../markdown_editor/plugins/insight/index.tsx | 166 ++++++++++++++++++ .../markdown_editor/plugins/osquery/index.tsx | 1 + 3 files changed, 171 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index 494ecb0c6b4d0..b66be05415e0a 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -13,6 +13,7 @@ import { import * as timelineMarkdownPlugin from './timeline'; import * as osqueryMarkdownPlugin from './osquery'; +import * as insightMarkdownPlugin from './insight'; export const { uiPlugins, parsingPlugins, processingPlugins } = { uiPlugins: getDefaultEuiMarkdownUiPlugins(), @@ -20,12 +21,15 @@ export const { uiPlugins, parsingPlugins, processingPlugins } = { processingPlugins: getDefaultEuiMarkdownProcessingPlugins(), }; +uiPlugins.push(insightMarkdownPlugin.plugin); uiPlugins.push(timelineMarkdownPlugin.plugin); uiPlugins.push(osqueryMarkdownPlugin.plugin); +parsingPlugins.push(insightMarkdownPlugin.parser); parsingPlugins.push(timelineMarkdownPlugin.parser); parsingPlugins.push(osqueryMarkdownPlugin.parser); // This line of code is TS-compatible and it will break if [1][1] change in the future. +processingPlugins[1][1].components.insight = insightMarkdownPlugin.renderer; processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer; processingPlugins[1][1].components.osquery = osqueryMarkdownPlugin.renderer; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx new file mode 100644 index 0000000000000..b17d48abd9112 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -0,0 +1,166 @@ +/* + * 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 { pickBy, isEmpty } from 'lodash'; +import type { Plugin } from 'unified'; +import React, { useContext, useMemo, useState, useCallback } from 'react'; +import type { RemarkTokenizer } from '@elastic/eui'; +import { + EuiSpacer, + EuiCodeBlock, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; +import { useForm, FormProvider } from 'react-hook-form'; +import styled from 'styled-components'; +import type { EuiMarkdownEditorUiPluginEditorProps } from '@elastic/eui/src/components/markdown_editor/markdown_types'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { + QueryOperator, + DataProviderType, + QueryMatch, + DataProvider, + DataProvidersAnd, +} from '@kbn/timelines-plugin/common'; +import { useKibana } from '../../../../lib/kibana'; +const x: InsightComponentProps = { + description: 'Please look for similar process by the same user.', + label: 'Similar Processes by User', + dataProviders: [{ field: 'process.name', value: 'process.Ext.user' }], +}; + +interface InsightComponentProps { + label?: string; + description?: string; + dataProviders: Array<{ field: string; value: string }>; +} + +export const parser: Plugin = function () { + const Parser = this.Parser; + const tokenizers = Parser.prototype.inlineTokenizers; + const methods = Parser.prototype.inlineMethods; + + const tokenizeInsight: RemarkTokenizer = function (eat, value, silent) { + if (value.startsWith('!{insight') === false) { + return false; + } + + const nextChar = value[9]; + if (nextChar !== '{' && nextChar !== '}') return false; + if (silent) { + return true; + } + + // is there a configuration? + const hasConfiguration = nextChar === '{'; + + let match = ''; + let configuration: InsightComponentProps = {}; + if (hasConfiguration) { + let configurationString = ''; + + let openObjects = 0; + + for (let i = 9; i < value.length; i++) { + const char = value[i]; + if (char === '{') { + openObjects++; + configurationString += char; + } else if (char === '}') { + openObjects--; + if (openObjects === -1) { + break; + } + configurationString += char; + } else { + configurationString += char; + } + } + + match += configurationString; + console.log(configurationString); + try { + configuration = JSON.parse(configurationString); + const dataProviders = {}; + // configuration.dataProviders; + + return eat(value)({ + type: 'insight', + ...configuration, + ...configuration.dataProviders.map((provider) => { + return { + [provider.field]: provider.value, + }; + }), + }); + } catch (e) { + console.log(e); + } + } + return false; + }; + + tokenizers.insight = tokenizeInsight; + methods.splice(methods.indexOf('text'), 0, 'insight'); +}; + +// receives the configuration from the parser and renders +const OpenInsightInTimeline = ({ + label, + description, + dataProviders, + ...fields +}: InsightComponentProps) => { + const handleOpen = useCallback(() => console.log('click run'), []); + console.log({ label, description, dataProviders, fields }); + return ( + <> + + {label ?? + i18n.translate('xpack.securitySolution.markdown.insights.openInsightButtonLabel', { + defaultMessage: 'Open Insight in Timeline', + })} + + + ); +}; + +export { OpenInsightInTimeline as renderer }; + +const InsightEditorComponent = ({ + node, + onSave, + onCancel, +}: EuiMarkdownEditorUiPluginEditorProps) => { + return ( +
+ +
+ ); +}; + +export const plugin = { + name: 'insight', + button: { + label: 'Insights', + iconType: 'timeline', + }, + helpText: ( +
+ + {'!{insight{options}}'} + + +
+ ), + editor: InsightEditorComponent, +}; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx index 3d046e349de31..d65405cd1c48f 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx @@ -258,6 +258,7 @@ const RunOsqueryButtonRenderer = ({ label?: string; query: string; ecs_mapping: { [key: string]: {} }; + test: []; }; }) => { const [showFlyout, setShowFlyout] = useState(false); From a6fb6faa815c0b1a81e0a29668aaa47590f3f18c Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Wed, 19 Oct 2022 17:28:39 +0000 Subject: [PATCH 02/21] WIP starting to work --- .../event_details/event_details.tsx | 2 +- .../investigation_guide_view.tsx | 5 +- .../table/investigate_in_timeline_button.tsx | 5 +- .../markdown_editor/plugins/index.ts | 32 +-- .../markdown_editor/plugins/insight/index.tsx | 189 ++++++++++++++++++ .../insight/use_insight_data_providers.ts | 83 ++++++++ .../plugins/insight/use_insight_query.ts | 88 ++++++++ .../markdown_editor/plugins/osquery/index.tsx | 1 + .../components/markdown_editor/renderer.tsx | 7 +- .../rules/step_about_rule_details/index.tsx | 9 +- .../side_panel/event_details/helpers.tsx | 5 +- .../public/timelines/containers/index.tsx | 4 +- .../containers/use_timeline_data_filters.ts | 19 +- .../common/search_strategy/timeline/index.ts | 2 +- .../timeline/factory/events/all/index.ts | 2 +- .../events/all/query.events_all.dsl.ts | 1 - 16 files changed, 417 insertions(+), 37 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index a0ef3b8904e3f..0caf3aaec6678 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -263,7 +263,7 @@ const EventDetailsComponent: React.FC = ({ )} - + ), } diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx index 4e9ff49a2b1dd..900e32289689f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx @@ -27,7 +27,8 @@ export const BasicAlertDataContext = createContext = ({ data }) => { + scopeId: string; +}> = ({ data, scopeId }) => { const ruleId = useMemo(() => { const item = data.find((d) => d.field === 'signal.rule.id' || d.field === ALERT_RULE_UUID); return Array.isArray(item?.originalValue) @@ -51,7 +52,7 @@ const InvestigationGuideViewComponent: React.FC<{ - {maybeRule.note} + {maybeRule.note} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx index 820488225e17b..919c552fd3c3d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx @@ -37,9 +37,12 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ getDataViewsSelector(state) ); + const hasTemplateProviders = + dataProviders && dataProviders.find((provider) => provider.type === 'template'); + const clearTimeline = useCreateTimeline({ timelineId: TimelineId.active, - timelineType: TimelineType.default, + timelineType: hasTemplateProviders ? TimelineType.template : TimelineType.default, }); const configureAndOpenTimeline = React.useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index 494ecb0c6b4d0..705f7915aede3 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -13,19 +13,27 @@ import { import * as timelineMarkdownPlugin from './timeline'; import * as osqueryMarkdownPlugin from './osquery'; +import * as insightMarkdownPlugin from './insight'; -export const { uiPlugins, parsingPlugins, processingPlugins } = { - uiPlugins: getDefaultEuiMarkdownUiPlugins(), - parsingPlugins: getDefaultEuiMarkdownParsingPlugins(), - processingPlugins: getDefaultEuiMarkdownProcessingPlugins(), -}; +export const markdownPlugins = (scopeId: string) => { + const { uiPlugins, parsingPlugins, processingPlugins } = { + uiPlugins: getDefaultEuiMarkdownUiPlugins(), + parsingPlugins: getDefaultEuiMarkdownParsingPlugins(), + processingPlugins: getDefaultEuiMarkdownProcessingPlugins(), + }; + + uiPlugins.push(insightMarkdownPlugin.plugin); + uiPlugins.push(timelineMarkdownPlugin.plugin); + uiPlugins.push(osqueryMarkdownPlugin.plugin); -uiPlugins.push(timelineMarkdownPlugin.plugin); -uiPlugins.push(osqueryMarkdownPlugin.plugin); + parsingPlugins.push(insightMarkdownPlugin.parser); + parsingPlugins.push(timelineMarkdownPlugin.parser); + parsingPlugins.push(osqueryMarkdownPlugin.parser); -parsingPlugins.push(timelineMarkdownPlugin.parser); -parsingPlugins.push(osqueryMarkdownPlugin.parser); + // This line of code is TS-compatible and it will break if [1][1] change in the future. + processingPlugins[1][1].components.insight = insightMarkdownPlugin.renderer(scopeId); + processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer; + processingPlugins[1][1].components.osquery = osqueryMarkdownPlugin.renderer; -// This line of code is TS-compatible and it will break if [1][1] change in the future. -processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer; -processingPlugins[1][1].components.osquery = osqueryMarkdownPlugin.renderer; + return { uiPlugins, parsingPlugins, processingPlugins }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx new file mode 100644 index 0000000000000..26880b4491fd3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -0,0 +1,189 @@ +/* + * 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 { pickBy, isEmpty } from 'lodash'; +import type { Plugin } from 'unified'; +import React, { useContext, useMemo, useState, useCallback } from 'react'; +import type { RemarkTokenizer } from '@elastic/eui'; +import { + EuiSpacer, + EuiCodeBlock, + EuiCallOut, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; +import { useForm, FormProvider } from 'react-hook-form'; +import styled from 'styled-components'; +import type { EuiMarkdownEditorUiPluginEditorProps } from '@elastic/eui/src/components/markdown_editor/markdown_types'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { + QueryOperator, + DataProviderType, + QueryMatch, + DataProvider, + DataProvidersAnd, +} from '@kbn/timelines-plugin/common'; +import { useKibana } from '../../../../lib/kibana'; +import { useInsightQuery } from './use_insight_query'; +import { useInsightDataProviders } from './use_insight_data_providers'; +import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; +import { InvestigateInTimelineButton } from '../../../event_details/table/investigate_in_timeline_button'; + +interface InsightComponentProps { + label: string; + description?: string; + providers?: Array<{ field: string; value: string; type: 'literal' | 'parameter' }>; +} + +export const parser: Plugin = function () { + const Parser = this.Parser; + const tokenizers = Parser.prototype.inlineTokenizers; + const methods = Parser.prototype.inlineMethods; + + const tokenizeInsight: RemarkTokenizer = function (eat, value, silent) { + if (value.startsWith('!{insight') === false) { + return false; + } + + const nextChar = value[9]; + if (nextChar !== '{' && nextChar !== '}') return false; + if (silent) { + return true; + } + + // is there a configuration? + const hasConfiguration = nextChar === '{'; + + let match = ''; + let configuration: InsightComponentProps = {}; + if (hasConfiguration) { + let configurationString = ''; + + let openObjects = 0; + + for (let i = 9; i < value.length; i++) { + const char = value[i]; + if (char === '{') { + openObjects++; + configurationString += char; + } else if (char === '}') { + openObjects--; + if (openObjects === -1) { + break; + } + configurationString += char; + } else { + configurationString += char; + } + } + + match += configurationString; + try { + configuration = JSON.parse(configurationString); + if (Array.isArray(configuration.providers)) { + const providerConfig = configuration.providers.reduce((prev, next) => { + return { + ...prev, + [next.field]: next, + }; + }, {}); + configuration = { ...configuration, ...providerConfig }; + delete configuration.providers; + } + return eat(value)({ + type: 'insight', + ...configuration, + }); + } catch (e) { + console.log(e); + } + } + return false; + }; + + tokenizers.insight = tokenizeInsight; + methods.splice(methods.indexOf('text'), 0, 'insight'); +}; + +// receives the configuration from the parser and renders +const OpenInsightInTimeline = (scopeId) => { + const InsightComponent = ({ + label, + description, + children, + position, + type, + ...providers + }: InsightComponentProps) => { + const { data: alertData, alertId } = useContext(BasicAlertDataContext); + console.log(alertData, alertId); + const providerGlob = useMemo(() => { + return Object.values(providers); + }, [providers]); + const { dataProviders } = useInsightDataProviders({ + providers: providerGlob, + scopeId, + alertData, + alertId, + }); + console.log({ dataProviders }); + const { totalCount, isQueryLoading } = useInsightQuery({ + dataProviders, + scopeId, + alertData, + }); + return ( + + {isQueryLoading === false ?

{`${totalCount} matching events`}

: null} +

{description}

+ + {label ?? + i18n.translate('xpack.securitySolution.markdown.insights.openInsightButtonLabel', { + defaultMessage: 'Open Insight in Timeline', + })} + +
+ ); + }; + return InsightComponent; +}; + +export { OpenInsightInTimeline as renderer }; + +const InsightEditorComponent = ({ + node, + onSave, + onCancel, +}: EuiMarkdownEditorUiPluginEditorProps) => { + return ( +
+ +
+ ); +}; + +export const plugin = { + name: 'insight', + button: { + label: 'Insights', + iconType: 'timeline', + }, + helpText: ( +
+ + {'!{insight{options}}'} + + +
+ ), + editor: InsightEditorComponent, +}; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts new file mode 100644 index 0000000000000..233441659468e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts @@ -0,0 +1,83 @@ +/* + * 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 { useMemo, useEffect, useRef, useState, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { noop } from 'lodash/fp'; +import deepEqual from 'fast-deep-equal'; +import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/public'; + +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; +import { Subscription } from 'rxjs'; + +import { useHttp, useKibana } from '../../../../lib/kibana'; +import { convertKueryToElasticSearchQuery } from '../../../../lib/kuery'; +import { useAppToasts } from '../../../../hooks/use_app_toasts'; +import { useSourcererDataView } from '../../../../containers/sourcerer'; +import type { inputsModel } from '../../../../store'; +import type { ESQuery } from '../../../../../../common/typed_json'; + +import { useTimelineDataFilters } from '../../../../../timelines/containers/use_timeline_data_filters'; +import { getDataProvider } from '../../../event_details/table/use_action_cell_data_provider'; +import { getEnrichedFieldInfo } from '../../../event_details/helpers'; +import { TimelineId } from '../../../../common/types/timeline'; +import { IS_OPERATOR } from '../../../../../timelines/components/timeline/data_providers/data_provider'; +import { + TimelineEventsQueries, + TimelineRequestOptionsPaginated, +} from '../../../../../../common/search_strategy'; + +export const useInsightDataProviders = ({ + providers, + scopeId, + alertData, + alertId, +}: UseInsightQuery): any => { + function getFieldValue(fields, fieldToFind) { + const alertField = fields.find((dataField) => dataField.field === fieldToFind); + return alertField.values ? alertField.values[0] : '*'; + } + const dataProviders = useMemo(() => { + if (alertData) { + return providers.map(({ field, value, type }) => { + return { + and: [], + enabled: true, + id: JSON.stringify(field + value + type), + name: field, + excluded: false, + kqlQuery: '', + type: 'default', + queryMatch: { + field, + value: type === 'parameter' ? getFieldValue(alertData, value) : value, + operator: IS_OPERATOR, + }, + }; + }); + } else { + return providers.map(({ field, value, type }) => { + return { + and: [], + enabled: true, + id: JSON.stringify(field + value + type), + name: field, + excluded: false, + kqlQuery: '', + type: type === 'parameter' ? 'template' : 'default', + queryMatch: { + field, + value: type === 'parameter' ? `{${value}}` : value, + operator: IS_OPERATOR, + }, + }; + }); + } + }, [alertData, providers]); + return { dataProviders }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts new file mode 100644 index 0000000000000..a754dd03287e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts @@ -0,0 +1,88 @@ +/* + * 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 { useMemo, useEffect, useRef, useState, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { noop } from 'lodash/fp'; +import deepEqual from 'fast-deep-equal'; +import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/public'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; + +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; +import { Subscription } from 'rxjs'; + +import type { DataProvider } from '@kbn/timelines-plugin/common'; +import { useHttp, useKibana } from '../../../../lib/kibana'; +import { convertKueryToElasticSearchQuery, combineQueries } from '../../../../lib/kuery'; +import { useAppToasts } from '../../../../hooks/use_app_toasts'; +import type { inputsModel } from '../../../../store'; +import type { ESQuery } from '../../../../../../common/typed_json'; + +import { useTimelineDataFilters } from '../../../../../timelines/containers/use_timeline_data_filters'; +import { useTimelineEvents } from '../../../../../timelines/containers'; +import { getDataProvider } from '../../../event_details/table/use_action_cell_data_provider'; +import { useSourcererDataView } from '../../../../containers/sourcerer'; +import { SourcererScopeName } from '../../../../store/sourcerer/model'; +import { TimelineId } from '../../../../common/types/timeline'; +import { + TimelineEventsQueries, + TimelineRequestOptionsPaginated, +} from '../../../../../../common/search_strategy'; + +interface UseInsightQuery { + dataProviders: DataProvider[]; + scopeId: string; + alertData: any; +} + +interface InsightRequest { + filterQuery: ESQuery | string | undefined; + defaultIndex: string[]; + factoryQueryType?: string; + entityType?: string; + fieldsRequested: string[]; +} + +interface InisghtResponse { + rawResponse: any; +} + +export const useInsightQuery = ({ dataProviders, scopeId, alertData }: UseInsightQuery): any => { + const { uiSettings } = useKibana().services; + const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); + const { browserFields, selectedPatterns, indexPattern, dataViewId } = useSourcererDataView( + SourcererScopeName.timeline + ); + const combinedQueries = combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters: [], + kqlQuery: { + query: '', + language: 'kuery', + }, + kqlMode: 'filter', + }); + const [isQueryLoading, { events, inspect, totalCount, pageInfo, loadPage, updatedAt, refetch }] = + useTimelineEvents({ + dataViewId, + fields: ['*'], + filterQuery: combinedQueries?.filterQuery, + id: 'timeline-1', + indexNames: selectedPatterns, + language: 'kuery', + limit: 1, + runtimeMappings: {}, + }); + return { + isQueryLoading, + totalCount, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx index 3d046e349de31..d65405cd1c48f 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/index.tsx @@ -258,6 +258,7 @@ const RunOsqueryButtonRenderer = ({ label?: string; query: string; ecs_mapping: { [key: string]: {} }; + test: []; }; }) => { const [showFlyout, setShowFlyout] = useState(false); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx index 6dcb93321056e..738db8e717b5c 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx @@ -10,21 +10,22 @@ import { cloneDeep } from 'lodash/fp'; import type { EuiLinkAnchorProps } from '@elastic/eui'; import { EuiMarkdownFormat } from '@elastic/eui'; -import { parsingPlugins, processingPlugins } from './plugins'; +import { markdownPlugins } from './plugins'; import { MarkdownLink } from './markdown_link'; interface Props { children: string; disableLinks?: boolean; + scopeId: string; } -const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) => { +const MarkdownRendererComponent: React.FC = ({ children, disableLinks, scopeId }) => { const MarkdownLinkProcessingComponent: React.FC = useMemo( // eslint-disable-next-line react/display-name () => (props) => , [disableLinks] ); - + const { processingPlugins, parsingPlugins } = markdownPlugins(scopeId); // Deep clone of the processing plugins to prevent affecting the markdown editor. const processingPluginList = cloneDeep(processingPlugins); // This line of code is TS-compatible and it will break if [1][1] change in the future. diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx index ac5e6c559d0d9..dc261adc694b5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx @@ -22,6 +22,7 @@ import styled from 'styled-components'; import { HeaderSection } from '../../../../common/components/header_section'; import { MarkdownRenderer } from '../../../../common/components/markdown_editor'; +import { TableId } from '../../../../../common/types/timeline'; import type { AboutStepRule, AboutStepRuleDetails, @@ -158,7 +159,9 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ maxHeight={aboutPanelHeight} className="eui-yScrollWithShadows" > - {stepDataDetails.note} + + {stepDataDetails.note} + )} @@ -171,7 +174,9 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ maxHeight={aboutPanelHeight} className="eui-yScrollWithShadows" > - {stepDataDetails.setup} + + {stepDataDetails.setup} + )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx index 6bee089027477..75b950f944d72 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx @@ -19,6 +19,7 @@ export interface GetBasicDataFromDetailsData { userName: string; ruleName: string; timestamp: string; + data: any; } export const useBasicDataFromDetailsData = ( @@ -62,8 +63,10 @@ export const useBasicDataFromDetailsData = ( userName, ruleName, timestamp, + // lol don't do this probably + data, }), - [agentId, alertId, hostName, isAlert, ruleName, timestamp, userName] + [agentId, alertId, hostName, isAlert, ruleName, timestamp, userName, data] ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 700d5d9d1255e..c06971bfb88ae 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -77,7 +77,7 @@ type TimelineResponse = T extends 'kuery' export interface UseTimelineEventsProps { dataViewId: string | null; - endDate: string; + endDate?: string; eqlOptions?: EqlOptionsSelected; fields: string[]; filterQuery?: ESQuery | string; @@ -88,7 +88,7 @@ export interface UseTimelineEventsProps { runtimeMappings: MappingRuntimeFields; skip?: boolean; sort?: TimelineRequestSortField[]; - startDate: string; + startDate?: string; timerangeKind?: 'absolute' | 'relative'; } diff --git a/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts b/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts index 7475660b9d848..66579988c2be3 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts @@ -6,6 +6,7 @@ */ import { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { @@ -14,8 +15,7 @@ import { endSelector, } from '../../common/components/super_date_picker/selectors'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; -import { useSourcererDataView } from '../../common/containers/sourcerer'; -import { sourcererSelectors } from '../../common/store'; +import { useSourcererDataView, getScopeFromPath } from '../../common/containers/sourcerer'; export function useTimelineDataFilters(isActiveTimelines: boolean) { const getStartSelector = useMemo(() => startSelector(), []); @@ -44,18 +44,17 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) { return getEndSelector(state.inputs.global); } }); - const getDefaultDataViewSelector = useMemo( - () => sourcererSelectors.defaultDataViewSelector(), - [] + + const { pathname } = useLocation(); + const { selectedPatterns: nonTimelinePatterns } = useSourcererDataView( + getScopeFromPath(pathname) ); - const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector); const { selectedPatterns: timelinePatterns } = useSourcererDataView(SourcererScopeName.timeline); - const selectedPatterns = useMemo( - () => (isActiveTimelines ? timelinePatterns : defaultDataView.patternList), - [defaultDataView.patternList, isActiveTimelines, timelinePatterns] - ); + const selectedPatterns = useMemo(() => { + return isActiveTimelines ? timelinePatterns : nonTimelinePatterns; + }, [isActiveTimelines, timelinePatterns, nonTimelinePatterns]); return { selectedPatterns, diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts index dfdc1ed3eabd4..a5856169a5748 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts @@ -32,7 +32,7 @@ export * from './events'; export type TimelineFactoryQueryTypes = TimelineEventsQueries; export interface TimelineRequestBasicOptions extends IEsSearchRequest { - timerange: TimerangeInput; + timerange?: TimerangeInput; filterQuery: ESQuery | string | undefined; defaultIndex: string[]; factoryQueryType?: TimelineFactoryQueryTypes; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts index 3cec04f52b671..895e9cd2ce5d6 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts @@ -38,6 +38,7 @@ export const timelineEventsAll: TimelineFactory = { // eslint-disable-next-line prefer-const let { fieldRequested, ...queryOptions } = cloneDeep(options); queryOptions.fields = buildFieldsRequest(fieldRequested, queryOptions.excludeEcsData); + const { activePage, querySize } = options.pagination; const producerBuckets = getOr([], 'aggregations.producers.buckets', response.rawResponse); const totalCount = response.rawResponse.hits.total || 0; @@ -80,7 +81,6 @@ export const timelineEventsAll: TimelineFactory = { consumers, inspect, edges, - // @ts-expect-error code doesn't handle TotalHits totalCount, pageInfo: { activePage: activePage ?? 0, diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts index 9f6902c65f639..5ae88bcf6f460 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts @@ -28,7 +28,6 @@ export const buildTimelineEventsAllQuery = ({ timerange, }: Omit) => { const filterClause = [...createQueryFilterClauses(filterQuery)]; - const getTimerangeFilter = (timerangeOption: TimerangeInput | undefined): TimerangeFilter[] => { if (timerangeOption) { const { to, from } = timerangeOption; From 5c75894ac5127ad17029a0e31fd7fef77909f398 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 1 Nov 2022 21:47:19 -0400 Subject: [PATCH 03/21] WIP query working minus timerange --- .../markdown_editor/plugins/insight/index.tsx | 19 ++++++------ .../insight/use_insight_data_providers.ts | 30 ++++++++++++------- .../side_panel/event_details/helpers.tsx | 2 +- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 26880b4491fd3..8068507b6a434 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -91,11 +91,17 @@ export const parser: Plugin = function () { configuration = JSON.parse(configurationString); if (Array.isArray(configuration.providers)) { const providerConfig = configuration.providers.reduce((prev, next) => { + const { type, ...fieldKeyValue } = next; + const [[field, value]] = Object.entries(fieldKeyValue); return { ...prev, - [next.field]: next, + [field]: { + //field, + value, + type, + }, }; - }, {}); + }, Object.create(null)); configuration = { ...configuration, ...providerConfig }; delete configuration.providers; } @@ -125,18 +131,13 @@ const OpenInsightInTimeline = (scopeId) => { ...providers }: InsightComponentProps) => { const { data: alertData, alertId } = useContext(BasicAlertDataContext); - console.log(alertData, alertId); - const providerGlob = useMemo(() => { - return Object.values(providers); - }, [providers]); const { dataProviders } = useInsightDataProviders({ - providers: providerGlob, + providers, scopeId, alertData, alertId, }); - console.log({ dataProviders }); - const { totalCount, isQueryLoading } = useInsightQuery({ + const { totalCount, isQueryLoading, oldestTimestamp } = useInsightQuery({ dataProviders, scopeId, alertData, diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts index 233441659468e..d78528ffe84d9 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts @@ -25,11 +25,18 @@ import type { ESQuery } from '../../../../../../common/typed_json'; import { useTimelineDataFilters } from '../../../../../timelines/containers/use_timeline_data_filters'; import { getDataProvider } from '../../../event_details/table/use_action_cell_data_provider'; import { getEnrichedFieldInfo } from '../../../event_details/helpers'; -import { TimelineId } from '../../../../common/types/timeline'; +import type { + QueryOperator, + QueryMatch, + DataProvider, + DataProvidersAnd, +} from '@kbn/timelines-plugin/common'; +import { DataProviderType } from '@kbn/timelines-plugin/common'; import { IS_OPERATOR } from '../../../../../timelines/components/timeline/data_providers/data_provider'; import { TimelineEventsQueries, TimelineRequestOptionsPaginated, + TimelineEventsDetailsItem, } from '../../../../../../common/search_strategy'; export const useInsightDataProviders = ({ @@ -37,14 +44,17 @@ export const useInsightDataProviders = ({ scopeId, alertData, alertId, -}: UseInsightQuery): any => { - function getFieldValue(fields, fieldToFind) { +}: { + providers: { [field: string]: { value: string; type: 'parameter' | 'value' } }; + alertData: TimelineEventsDetailsItem[]; +}): { dataProviders: DataProvider[] } => { + function getFieldValue(fields: TimelineEventsDetailsItem[], fieldToFind: string) { const alertField = fields.find((dataField) => dataField.field === fieldToFind); - return alertField.values ? alertField.values[0] : '*'; + return alertField?.values ? alertField.values[0] : '*'; } const dataProviders = useMemo(() => { if (alertData) { - return providers.map(({ field, value, type }) => { + return Object.entries(providers).map(([field, { value, type }]) => { return { and: [], enabled: true, @@ -52,16 +62,16 @@ export const useInsightDataProviders = ({ name: field, excluded: false, kqlQuery: '', - type: 'default', + type: DataProviderType.default, queryMatch: { field, value: type === 'parameter' ? getFieldValue(alertData, value) : value, - operator: IS_OPERATOR, + operator: IS_OPERATOR as QueryOperator, }, }; }); } else { - return providers.map(({ field, value, type }) => { + return Object.entries(providers).map(([field, { value, type }]) => { return { and: [], enabled: true, @@ -69,11 +79,11 @@ export const useInsightDataProviders = ({ name: field, excluded: false, kqlQuery: '', - type: type === 'parameter' ? 'template' : 'default', + type: type === 'parameter' ? DataProviderType.template : DataProviderType.default, queryMatch: { field, value: type === 'parameter' ? `{${value}}` : value, - operator: IS_OPERATOR, + operator: IS_OPERATOR as QueryOperator, }, }; }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx index 75b950f944d72..6454279d928b8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx @@ -19,7 +19,7 @@ export interface GetBasicDataFromDetailsData { userName: string; ruleName: string; timestamp: string; - data: any; + data: TimelineEventsDetailsItem[] | null; } export const useBasicDataFromDetailsData = ( From 009678572a9349dd971b33dd62a67514d9e9e66a Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 1 Nov 2022 23:02:36 -0400 Subject: [PATCH 04/21] Mostly working --- .../table/investigate_in_timeline_button.tsx | 21 ++++++++++++++----- .../markdown_editor/plugins/insight/index.tsx | 7 +++++-- .../plugins/insight/use_insight_query.ts | 5 +++++ .../containers/use_timeline_data_filters.ts | 13 +++++++++--- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx index 919c552fd3c3d..41ca08cc893f6 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; import { useDispatch } from 'react-redux'; @@ -13,6 +13,7 @@ import { useDispatch } from 'react-redux'; import { sourcererSelectors } from '../../../store'; import { InputsModelId } from '../../../store/inputs/constants'; import { inputsActions } from '../../../store/inputs'; +import type { TimeRange } from '../../../store/inputs/model'; import { updateProviders, setFilters } from '../../../../timelines/store/timeline/actions'; import { sourcererActions } from '../../../store/actions'; import { SourcererScopeName } from '../../../store/sourcerer/model'; @@ -21,12 +22,14 @@ import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { useCreateTimeline } from '../../../../timelines/components/timeline/properties/use_create_timeline'; import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import { getDataProvider } from './use_action_cell_data_provider'; export const InvestigateInTimelineButton: React.FunctionComponent<{ asEmptyButton: boolean; dataProviders: DataProvider[] | null; filters?: Filter[] | null; -}> = ({ asEmptyButton, children, dataProviders, filters, ...rest }) => { + timeRange?: string; +}> = ({ asEmptyButton, children, dataProviders, filters, timeRange, ...rest }) => { const dispatch = useDispatch(); const getDataViewsSelector = useMemo( @@ -45,10 +48,10 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ timelineType: hasTemplateProviders ? TimelineType.template : TimelineType.default, }); - const configureAndOpenTimeline = React.useCallback(() => { + const configureAndOpenTimeline = useCallback(() => { if (dataProviders || filters) { // Reset the current timeline - clearTimeline(); + clearTimeline({ timeRange }); if (dataProviders) { // Update the timeline's providers to match the current prevalence field query dispatch( @@ -79,7 +82,15 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ // Unlock the time range from the global time range dispatch(inputsActions.removeLinkTo([InputsModelId.timeline, InputsModelId.global])); } - }, [dataProviders, clearTimeline, dispatch, defaultDataView.id, signalIndexName, filters]); + }, [ + dataProviders, + clearTimeline, + dispatch, + defaultDataView.id, + signalIndexName, + filters, + timeRange, + ]); return asEmptyButton ? ( { {isQueryLoading === false ?

{`${totalCount} matching events`}

: null}

{description}

- + {label ?? i18n.translate('xpack.securitySolution.markdown.insights.openInsightButtonLabel', { defaultMessage: 'Open Insight in Timeline', diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts index a754dd03287e5..5c2084180c332 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts @@ -81,8 +81,13 @@ export const useInsightQuery = ({ dataProviders, scopeId, alertData }: UseInsigh limit: 1, runtimeMappings: {}, }); + const [oldestEvent] = events; + const timestamp = + oldestEvent && oldestEvent.data && oldestEvent.data.find((d) => d.field === '@timestamp'); + const oldestTimestamp = timestamp && timestamp.value && timestamp.value[0]; return { isQueryLoading, totalCount, + oldestTimestamp, }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts b/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts index 66579988c2be3..6a242e9a323e7 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/use_timeline_data_filters.ts @@ -16,6 +16,7 @@ import { } from '../../common/components/super_date_picker/selectors'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useSourcererDataView, getScopeFromPath } from '../../common/containers/sourcerer'; +import { sourcererSelectors } from '../../common/store'; export function useTimelineDataFilters(isActiveTimelines: boolean) { const getStartSelector = useMemo(() => startSelector(), []); @@ -44,7 +45,11 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) { return getEndSelector(state.inputs.global); } }); - + const getDefaultDataViewSelector = useMemo( + () => sourcererSelectors.defaultDataViewSelector(), + [] + ); + const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector); const { pathname } = useLocation(); const { selectedPatterns: nonTimelinePatterns } = useSourcererDataView( getScopeFromPath(pathname) @@ -53,8 +58,10 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) { const { selectedPatterns: timelinePatterns } = useSourcererDataView(SourcererScopeName.timeline); const selectedPatterns = useMemo(() => { - return isActiveTimelines ? timelinePatterns : nonTimelinePatterns; - }, [isActiveTimelines, timelinePatterns, nonTimelinePatterns]); + return isActiveTimelines + ? [...new Set([...timelinePatterns, ...defaultDataView.patternList])] + : [...new Set([...nonTimelinePatterns, ...defaultDataView.patternList])]; + }, [isActiveTimelines, timelinePatterns, nonTimelinePatterns, defaultDataView.patternList]); return { selectedPatterns, From 95b177f200b01782e78f895833f38dd802a666b4 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 8 Nov 2022 17:52:11 -0500 Subject: [PATCH 05/21] WIP --- .../table/investigate_in_timeline_button.tsx | 19 +++++++++++-------- .../markdown_editor/plugins/insight/index.tsx | 1 + 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx index 41ca08cc893f6..8bf32a8057c4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx @@ -29,7 +29,8 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ dataProviders: DataProvider[] | null; filters?: Filter[] | null; timeRange?: string; -}> = ({ asEmptyButton, children, dataProviders, filters, timeRange, ...rest }) => { + keepDataView?: boolean; +}> = ({ asEmptyButton, children, dataProviders, filters, timeRange, keepDataView, ...rest }) => { const dispatch = useDispatch(); const getDataViewsSelector = useMemo( @@ -72,13 +73,15 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ } // Only show detection alerts // (This is required so the timeline event count matches the prevalence count) - dispatch( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.timeline, - selectedDataViewId: defaultDataView.id, - selectedPatterns: [signalIndexName || ''], - }) - ); + if (!keepDataView) { + dispatch( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: defaultDataView.id, + selectedPatterns: [signalIndexName || ''], + }) + ); + } // Unlock the time range from the global time range dispatch(inputsActions.removeLinkTo([InputsModelId.timeline, InputsModelId.global])); } diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index e33a381d918b4..02ca57fb3a043 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -149,6 +149,7 @@ const OpenInsightInTimeline = (scopeId) => { asEmptyButton={false} dataProviders={dataProviders} timeRange={oldestTimestamp} + keepDataView={false} > {label ?? i18n.translate('xpack.securitySolution.markdown.insights.openInsightButtonLabel', { From 3d065c31f3e5816a04e9fa6a46230eb07be519c9 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 13:16:53 +0000 Subject: [PATCH 06/21] Fix types, code working --- .../investigation_guide_view.tsx | 2 +- .../table/investigate_in_timeline_button.tsx | 17 +- .../markdown_editor/plugins/index.ts | 33 ++-- .../markdown_editor/plugins/insight/index.tsx | 122 ++++++-------- .../insight/use_insight_data_providers.ts | 150 ++++++++++-------- .../plugins/insight/use_insight_query.ts | 68 +++----- .../components/markdown_editor/renderer.tsx | 6 +- .../rules/step_about_rule_details/index.tsx | 9 +- .../side_panel/event_details/helpers.tsx | 1 - .../search_strategy/timeline/eql/helpers.ts | 22 +-- .../timeline/factory/events/all/index.ts | 1 + 11 files changed, 192 insertions(+), 239 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx index b50837d53d714..2cbcc0d670631 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx @@ -52,7 +52,7 @@ const InvestigationGuideViewComponent: React.FC<{ - {maybeRule.note} + {maybeRule.note} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx index 8bf32a8057c4c..1f8a2129d5e18 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx @@ -13,7 +13,6 @@ import { useDispatch } from 'react-redux'; import { sourcererSelectors } from '../../../store'; import { InputsModelId } from '../../../store/inputs/constants'; import { inputsActions } from '../../../store/inputs'; -import type { TimeRange } from '../../../store/inputs/model'; import { updateProviders, setFilters } from '../../../../timelines/store/timeline/actions'; import { sourcererActions } from '../../../store/actions'; import { SourcererScopeName } from '../../../store/sourcerer/model'; @@ -22,13 +21,12 @@ import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { useCreateTimeline } from '../../../../timelines/components/timeline/properties/use_create_timeline'; import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; -import { getDataProvider } from './use_action_cell_data_provider'; export const InvestigateInTimelineButton: React.FunctionComponent<{ asEmptyButton: boolean; dataProviders: DataProvider[] | null; filters?: Filter[] | null; - timeRange?: string; + timeRange?: string | null; keepDataView?: boolean; }> = ({ asEmptyButton, children, dataProviders, filters, timeRange, keepDataView, ...rest }) => { const dispatch = useDispatch(); @@ -52,7 +50,17 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ const configureAndOpenTimeline = useCallback(() => { if (dataProviders || filters) { // Reset the current timeline - clearTimeline({ timeRange }); + if (timeRange) { + clearTimeline({ + timeRange: { + kind: 'absolute', + from: timeRange, + to: new Date().toISOString(), + }, + }); + } else { + clearTimeline(); + } if (dataProviders) { // Update the timeline's providers to match the current prevalence field query dispatch( @@ -93,6 +101,7 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ signalIndexName, filters, timeRange, + keepDataView, ]); return asEmptyButton ? ( diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index 705f7915aede3..8a76223bb290c 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -15,25 +15,20 @@ import * as timelineMarkdownPlugin from './timeline'; import * as osqueryMarkdownPlugin from './osquery'; import * as insightMarkdownPlugin from './insight'; -export const markdownPlugins = (scopeId: string) => { - const { uiPlugins, parsingPlugins, processingPlugins } = { - uiPlugins: getDefaultEuiMarkdownUiPlugins(), - parsingPlugins: getDefaultEuiMarkdownParsingPlugins(), - processingPlugins: getDefaultEuiMarkdownProcessingPlugins(), - }; - - uiPlugins.push(insightMarkdownPlugin.plugin); - uiPlugins.push(timelineMarkdownPlugin.plugin); - uiPlugins.push(osqueryMarkdownPlugin.plugin); +export const { uiPlugins, parsingPlugins, processingPlugins } = { + uiPlugins: getDefaultEuiMarkdownUiPlugins(), + parsingPlugins: getDefaultEuiMarkdownParsingPlugins(), + processingPlugins: getDefaultEuiMarkdownProcessingPlugins(), +}; - parsingPlugins.push(insightMarkdownPlugin.parser); - parsingPlugins.push(timelineMarkdownPlugin.parser); - parsingPlugins.push(osqueryMarkdownPlugin.parser); +uiPlugins.push(timelineMarkdownPlugin.plugin); +uiPlugins.push(osqueryMarkdownPlugin.plugin); - // This line of code is TS-compatible and it will break if [1][1] change in the future. - processingPlugins[1][1].components.insight = insightMarkdownPlugin.renderer(scopeId); - processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer; - processingPlugins[1][1].components.osquery = osqueryMarkdownPlugin.renderer; +parsingPlugins.push(insightMarkdownPlugin.parser); +parsingPlugins.push(timelineMarkdownPlugin.parser); +parsingPlugins.push(osqueryMarkdownPlugin.parser); - return { uiPlugins, parsingPlugins, processingPlugins }; -}; +// This line of code is TS-compatible and it will break if [1][1] change in the future. +processingPlugins[1][1].components.insight = insightMarkdownPlugin.renderer; +processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer; +processingPlugins[1][1].components.osquery = osqueryMarkdownPlugin.renderer; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 1d7e11d542006..874871bde3a3a 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -6,39 +6,18 @@ */ import type { Plugin } from 'unified'; -import React, { useContext, useMemo, useState, useCallback } from 'react'; +import React, { useContext } from 'react'; import type { RemarkTokenizer } from '@elastic/eui'; -import { - EuiSpacer, - EuiCodeBlock, - EuiCallOut, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiModalFooter, - EuiButton, - EuiButtonEmpty, -} from '@elastic/eui'; -import { useForm, FormProvider } from 'react-hook-form'; -import styled from 'styled-components'; +import { EuiSpacer, EuiCodeBlock, EuiLoadingSpinner, EuiIcon } from '@elastic/eui'; import type { EuiMarkdownEditorUiPluginEditorProps } from '@elastic/eui/src/components/markdown_editor/markdown_types'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import type { - QueryOperator, - DataProviderType, - QueryMatch, - DataProvider, - DataProvidersAnd, -} from '@kbn/timelines-plugin/common'; -import { useKibana } from '../../../../lib/kibana'; +import { useAppToasts } from '../../../../hooks/use_app_toasts'; import { useInsightQuery } from './use_insight_query'; import { useInsightDataProviders } from './use_insight_data_providers'; import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; import { InvestigateInTimelineButton } from '../../../event_details/table/investigate_in_timeline_button'; interface InsightComponentProps { - label: string; + label?: string; description?: string; providers?: string; } @@ -49,11 +28,12 @@ export const parser: Plugin = function () { const methods = Parser.prototype.inlineMethods; const tokenizeInsight: RemarkTokenizer = function (eat, value, silent) { - if (value.startsWith('!{insight') === false) { + const insightPrefix = '!{insight'; + if (value.startsWith(insightPrefix) === false) { return false; } - const nextChar = value[9]; + const nextChar = value[insightPrefix.length]; if (nextChar !== '{' && nextChar !== '}') return false; if (silent) { return true; @@ -62,14 +42,14 @@ export const parser: Plugin = function () { // is there a configuration? const hasConfiguration = nextChar === '{'; - let match = ''; let configuration: InsightComponentProps = {}; if (hasConfiguration) { let configurationString = ''; + let match = ''; let openObjects = 0; - for (let i = 9; i < value.length; i++) { + for (let i = insightPrefix.length; i < value.length; i++) { const char = value[i]; if (char === '{') { openObjects++; @@ -88,14 +68,17 @@ export const parser: Plugin = function () { match += configurationString; try { configuration = JSON.parse(configurationString); - console.log({configuration}); return eat(value)({ type: 'insight', ...configuration, providers: JSON.stringify(configuration.providers), }); } catch (e) { - console.log(e); + const now = eat.now(); + this.file.fail(`Unable to parse insight JSON configuration: ${e}`, { + line: now.line, + column: now.column + insightPrefix.length, + }); } } return false; @@ -106,57 +89,42 @@ export const parser: Plugin = function () { }; // receives the configuration from the parser and renders -const OpenInsightInTimeline = (scopeId) => { - const InsightComponent = ({ - label, - description, - children, - position, - type, - providers, - }: InsightComponentProps) => { - let parsedProviders = {}; - try { - if (providers !== undefined) { - parsedProviders = JSON.parse(providers); - } - } catch (err) { +const InsightComponent = ({ label, description, providers }: InsightComponentProps) => { + const { addError } = useAppToasts(); + let parsedProviders = []; + try { + if (providers !== undefined) { + parsedProviders = JSON.parse(providers); } - const { data: alertData, alertId } = useContext(BasicAlertDataContext); - console.log({parsedProviders}); - const { dataProviders } = useInsightDataProviders({ - providers, - scopeId, - alertData, - alertId, - }); - const { totalCount, isQueryLoading, oldestTimestamp } = useInsightQuery({ - dataProviders, - scopeId, - alertData, - }); + } catch (err) { + addError(err, { title: 'parse failure' }); + } + const { data: alertData } = useContext(BasicAlertDataContext); + const dataProviders = useInsightDataProviders({ + providers: parsedProviders, + alertData, + }); + const { totalCount, isQueryLoading, oldestTimestamp } = useInsightQuery({ + dataProviders, + }); + if (isQueryLoading) { + return ; + } else { return ( - - {isQueryLoading === false ?

{`${totalCount} matching events`}

: null} -

{description}

- - {label ?? - i18n.translate('xpack.securitySolution.markdown.insights.openInsightButtonLabel', { - defaultMessage: 'Open Insight in Timeline', - })} - -
+ + + {` ${label} (${totalCount}) - ${description}`} + ); - }; - return InsightComponent; + } }; -export { OpenInsightInTimeline as renderer }; +export { InsightComponent as renderer }; const InsightEditorComponent = ({ node, diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts index d78528ffe84d9..c9abfbcd9972e 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts @@ -5,89 +5,103 @@ * 2.0. */ -import { useMemo, useEffect, useRef, useState, useCallback } from 'react'; -import { useLocation } from 'react-router-dom'; -import { noop } from 'lodash/fp'; -import deepEqual from 'fast-deep-equal'; -import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/public'; - -import { useQuery } from '@tanstack/react-query'; -import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; -import { Subscription } from 'rxjs'; - -import { useHttp, useKibana } from '../../../../lib/kibana'; -import { convertKueryToElasticSearchQuery } from '../../../../lib/kuery'; -import { useAppToasts } from '../../../../hooks/use_app_toasts'; -import { useSourcererDataView } from '../../../../containers/sourcerer'; -import type { inputsModel } from '../../../../store'; -import type { ESQuery } from '../../../../../../common/typed_json'; - -import { useTimelineDataFilters } from '../../../../../timelines/containers/use_timeline_data_filters'; -import { getDataProvider } from '../../../event_details/table/use_action_cell_data_provider'; -import { getEnrichedFieldInfo } from '../../../event_details/helpers'; -import type { - QueryOperator, - QueryMatch, - DataProvider, - DataProvidersAnd, -} from '@kbn/timelines-plugin/common'; +import { useMemo } from 'react'; +import type { QueryOperator, DataProvider } from '@kbn/timelines-plugin/common'; import { DataProviderType } from '@kbn/timelines-plugin/common'; import { IS_OPERATOR } from '../../../../../timelines/components/timeline/data_providers/data_provider'; -import { - TimelineEventsQueries, - TimelineRequestOptionsPaginated, - TimelineEventsDetailsItem, -} from '../../../../../../common/search_strategy'; +import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; export const useInsightDataProviders = ({ providers, - scopeId, alertData, - alertId, }: { - providers: { [field: string]: { value: string; type: 'parameter' | 'value' } }; - alertData: TimelineEventsDetailsItem[]; -}): { dataProviders: DataProvider[] } => { + providers: Array>; + alertData?: TimelineEventsDetailsItem[] | null; +}): DataProvider[] => { function getFieldValue(fields: TimelineEventsDetailsItem[], fieldToFind: string) { const alertField = fields.find((dataField) => dataField.field === fieldToFind); return alertField?.values ? alertField.values[0] : '*'; } - const dataProviders = useMemo(() => { + const dataProviders: DataProvider[] = useMemo(() => { if (alertData) { - return Object.entries(providers).map(([field, { value, type }]) => { - return { - and: [], - enabled: true, - id: JSON.stringify(field + value + type), - name: field, - excluded: false, - kqlQuery: '', - type: DataProviderType.default, - queryMatch: { - field, - value: type === 'parameter' ? getFieldValue(alertData, value) : value, - operator: IS_OPERATOR as QueryOperator, - }, - }; + return providers.map((innerProvider) => { + return innerProvider.reduce((prev, next, index): DataProvider => { + const { field, value, type } = next; + if (index === 0) { + return { + and: [], + enabled: true, + id: JSON.stringify(field + value + type), + name: field, + excluded: false, + kqlQuery: '', + type: DataProviderType.default, + queryMatch: { + field, + value: type === 'parameter' ? getFieldValue(alertData, value) : value, + operator: IS_OPERATOR as QueryOperator, + }, + }; + } else { + const newProvider = { + and: [], + enabled: true, + id: JSON.stringify(field + value + type), + name: field, + excluded: false, + kqlQuery: '', + type: DataProviderType.default, + queryMatch: { + field, + value: type === 'parameter' ? getFieldValue(alertData, value) : value, + operator: IS_OPERATOR as QueryOperator, + }, + }; + prev.and.push(newProvider); + } + return prev; + }, {} as DataProvider); }); } else { - return Object.entries(providers).map(([field, { value, type }]) => { - return { - and: [], - enabled: true, - id: JSON.stringify(field + value + type), - name: field, - excluded: false, - kqlQuery: '', - type: type === 'parameter' ? DataProviderType.template : DataProviderType.default, - queryMatch: { - field, - value: type === 'parameter' ? `{${value}}` : value, - operator: IS_OPERATOR as QueryOperator, - }, - }; + return providers.map((innerProvider) => { + return innerProvider.reduce((prev, next, index) => { + const { field, value, type } = next; + if (index === 0) { + return { + and: [], + enabled: true, + id: JSON.stringify(field + value + type), + name: field, + excluded: false, + kqlQuery: '', + type: type === 'parameter' ? DataProviderType.template : DataProviderType.default, + queryMatch: { + field, + value: type === 'parameter' ? `{${value}}` : value, + operator: IS_OPERATOR as QueryOperator, + }, + }; + } else { + const newProvider = { + and: [], + enabled: true, + id: JSON.stringify(field + value + type), + name: field, + excluded: false, + kqlQuery: '', + type: type === 'parameter' ? DataProviderType.template : DataProviderType.default, + queryMatch: { + field, + value: type === 'parameter' ? `{${value}}` : value, + operator: IS_OPERATOR as QueryOperator, + }, + }; + prev.and.push(newProvider); + } + return prev; + }, {} as DataProvider); }); } }, [alertData, providers]); - return { dataProviders }; + return dataProviders; }; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts index 5c2084180c332..15397f4594e6c 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts @@ -5,54 +5,27 @@ * 2.0. */ -import { useMemo, useEffect, useRef, useState, useCallback } from 'react'; -import { useLocation } from 'react-router-dom'; -import { noop } from 'lodash/fp'; -import deepEqual from 'fast-deep-equal'; -import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/public'; +import { useMemo } from 'react'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; -import { useQuery } from '@tanstack/react-query'; -import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; -import { Subscription } from 'rxjs'; - import type { DataProvider } from '@kbn/timelines-plugin/common'; -import { useHttp, useKibana } from '../../../../lib/kibana'; -import { convertKueryToElasticSearchQuery, combineQueries } from '../../../../lib/kuery'; -import { useAppToasts } from '../../../../hooks/use_app_toasts'; -import type { inputsModel } from '../../../../store'; -import type { ESQuery } from '../../../../../../common/typed_json'; - -import { useTimelineDataFilters } from '../../../../../timelines/containers/use_timeline_data_filters'; +import { useKibana } from '../../../../lib/kibana'; +import { combineQueries } from '../../../../lib/kuery'; import { useTimelineEvents } from '../../../../../timelines/containers'; -import { getDataProvider } from '../../../event_details/table/use_action_cell_data_provider'; import { useSourcererDataView } from '../../../../containers/sourcerer'; import { SourcererScopeName } from '../../../../store/sourcerer/model'; -import { TimelineId } from '../../../../common/types/timeline'; -import { - TimelineEventsQueries, - TimelineRequestOptionsPaginated, -} from '../../../../../../common/search_strategy'; interface UseInsightQuery { dataProviders: DataProvider[]; - scopeId: string; - alertData: any; -} - -interface InsightRequest { - filterQuery: ESQuery | string | undefined; - defaultIndex: string[]; - factoryQueryType?: string; - entityType?: string; - fieldsRequested: string[]; } -interface InisghtResponse { - rawResponse: any; -} - -export const useInsightQuery = ({ dataProviders, scopeId, alertData }: UseInsightQuery): any => { +export const useInsightQuery = ({ + dataProviders, +}: UseInsightQuery): { + isQueryLoading: boolean; + totalCount: number; + oldestTimestamp: string | null | undefined; +} => { const { uiSettings } = useKibana().services; const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); const { browserFields, selectedPatterns, indexPattern, dataViewId } = useSourcererDataView( @@ -70,17 +43,16 @@ export const useInsightQuery = ({ dataProviders, scopeId, alertData }: UseInsigh }, kqlMode: 'filter', }); - const [isQueryLoading, { events, inspect, totalCount, pageInfo, loadPage, updatedAt, refetch }] = - useTimelineEvents({ - dataViewId, - fields: ['*'], - filterQuery: combinedQueries?.filterQuery, - id: 'timeline-1', - indexNames: selectedPatterns, - language: 'kuery', - limit: 1, - runtimeMappings: {}, - }); + const [isQueryLoading, { events, totalCount }] = useTimelineEvents({ + dataViewId, + fields: ['*'], + filterQuery: combinedQueries?.filterQuery, + id: 'timeline-1', + indexNames: selectedPatterns, + language: 'kuery', + limit: 1, + runtimeMappings: {}, + }); const [oldestEvent] = events; const timestamp = oldestEvent && oldestEvent.data && oldestEvent.data.find((d) => d.field === '@timestamp'); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx index 738db8e717b5c..449e2adee8bf8 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.tsx @@ -10,22 +10,20 @@ import { cloneDeep } from 'lodash/fp'; import type { EuiLinkAnchorProps } from '@elastic/eui'; import { EuiMarkdownFormat } from '@elastic/eui'; -import { markdownPlugins } from './plugins'; +import { parsingPlugins, processingPlugins } from './plugins'; import { MarkdownLink } from './markdown_link'; interface Props { children: string; disableLinks?: boolean; - scopeId: string; } -const MarkdownRendererComponent: React.FC = ({ children, disableLinks, scopeId }) => { +const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) => { const MarkdownLinkProcessingComponent: React.FC = useMemo( // eslint-disable-next-line react/display-name () => (props) => , [disableLinks] ); - const { processingPlugins, parsingPlugins } = markdownPlugins(scopeId); // Deep clone of the processing plugins to prevent affecting the markdown editor. const processingPluginList = cloneDeep(processingPlugins); // This line of code is TS-compatible and it will break if [1][1] change in the future. diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx index dc261adc694b5..ac5e6c559d0d9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx @@ -22,7 +22,6 @@ import styled from 'styled-components'; import { HeaderSection } from '../../../../common/components/header_section'; import { MarkdownRenderer } from '../../../../common/components/markdown_editor'; -import { TableId } from '../../../../../common/types/timeline'; import type { AboutStepRule, AboutStepRuleDetails, @@ -159,9 +158,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ maxHeight={aboutPanelHeight} className="eui-yScrollWithShadows" > - - {stepDataDetails.note} - + {stepDataDetails.note} )} @@ -174,9 +171,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ maxHeight={aboutPanelHeight} className="eui-yScrollWithShadows" > - - {stepDataDetails.setup} - + {stepDataDetails.setup} )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx index 6454279d928b8..f91a9eed0d165 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx @@ -63,7 +63,6 @@ export const useBasicDataFromDetailsData = ( userName, ruleName, timestamp, - // lol don't do this probably data, }), [agentId, alertId, hostName, isAlert, ruleName, timestamp, userName, data] diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts index 5424568c44a70..e9a2ef7e49cda 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts @@ -27,17 +27,19 @@ export const buildEqlDsl = (options: TimelineEqlRequestOptions): Record = { consumers, inspect, edges, + // @ts-expect-error code doesn't handle TotalHits totalCount, pageInfo: { activePage: activePage ?? 0, From 7f55bb72aaaaf661b651cc985e775921d672eac7 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 13:33:35 +0000 Subject: [PATCH 07/21] Remove unused variable --- .../markdown_editor/plugins/insight/index.tsx | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 874871bde3a3a..1645de090dbaa 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -10,6 +10,7 @@ import React, { useContext } from 'react'; import type { RemarkTokenizer } from '@elastic/eui'; import { EuiSpacer, EuiCodeBlock, EuiLoadingSpinner, EuiIcon } from '@elastic/eui'; import type { EuiMarkdownEditorUiPluginEditorProps } from '@elastic/eui/src/components/markdown_editor/markdown_types'; +import { i18n } from '@kbn/i18n'; import { useAppToasts } from '../../../../hooks/use_app_toasts'; import { useInsightQuery } from './use_insight_query'; import { useInsightDataProviders } from './use_insight_data_providers'; @@ -45,8 +46,6 @@ export const parser: Plugin = function () { let configuration: InsightComponentProps = {}; if (hasConfiguration) { let configurationString = ''; - let match = ''; - let openObjects = 0; for (let i = insightPrefix.length; i < value.length; i++) { @@ -65,7 +64,6 @@ export const parser: Plugin = function () { } } - match += configurationString; try { configuration = JSON.parse(configurationString); return eat(value)({ @@ -73,12 +71,18 @@ export const parser: Plugin = function () { ...configuration, providers: JSON.stringify(configuration.providers), }); - } catch (e) { + } catch (err) { const now = eat.now(); - this.file.fail(`Unable to parse insight JSON configuration: ${e}`, { - line: now.line, - column: now.column + insightPrefix.length, - }); + this.file.fail( + i18n.translate('xpack.securitySolution.markdownEditor.plugins.insightConfigError', { + values: { err }, + defaultMessage: 'Unable to parse insight JSON configuration: {err}', + }), + { + line: now.line, + column: now.column + insightPrefix.length, + } + ); } } return false; @@ -97,7 +101,11 @@ const InsightComponent = ({ label, description, providers }: InsightComponentPro parsedProviders = JSON.parse(providers); } } catch (err) { - addError(err, { title: 'parse failure' }); + addError(err, { + title: i18n.translate('xpack.securitySolution.markdownEditor.plugins.insightProviderError', { + defaultMessage: 'Unable to parse insight provider configuration', + }), + }); } const { data: alertData } = useContext(BasicAlertDataContext); const dataProviders = useInsightDataProviders({ From 5fd36e761054d3f68505bd2a0d0a55f29164898b Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 13:56:09 +0000 Subject: [PATCH 08/21] Remove unused prop --- .../public/common/components/event_details/event_details.tsx | 2 +- .../components/event_details/investigation_guide_view.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index f319e299af955..622d3035a7de6 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -304,7 +304,7 @@ const EventDetailsComponent: React.FC = ({ )} - + ), } diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx index 2cbcc0d670631..5e2827c396f74 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx @@ -27,8 +27,7 @@ export const BasicAlertDataContext = createContext = ({ data, scopeId }) => { +}> = ({ data }) => { const ruleId = useMemo(() => { const item = data.find((d) => d.field === 'signal.rule.id' || d.field === ALERT_RULE_UUID); return Array.isArray(item?.originalValue) From 3a6b4f6f1fed90e5154cb76aa8fb6fb0fc465264 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 15:07:16 +0000 Subject: [PATCH 09/21] Add useDataProvider tests --- .../use_insight_data_providers.test.ts | 134 ++++++++++++++++++ .../insight/use_insight_data_providers.ts | 15 +- 2 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts new file mode 100644 index 0000000000000..580968141994b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import type { DataProvider } from '@kbn/timelines-plugin/common'; +import type { UseInsightDataProvidersProps, Provider } from './use_insight_data_providers'; +import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; +import { useInsightDataProviders } from './use_insight_data_providers'; +import { mockAlertDetailsData } from '../../../event_details/__mocks__'; + +const mockAlertDetailsDataWithIsObject = mockAlertDetailsData.map((detail) => { + return { + ...detail, + isObjectArray: false, + }; +}) as TimelineEventsDetailsItem[]; + +const nestedAndProvider = [ + [ + { + field: 'event.id', + value: 'kibana.alert.rule.uuid', + type: 'parameter', + }, + ], + [ + { + field: 'event.category', + value: 'network', + type: 'literal', + }, + { + field: 'process.pid', + value: 'process.pid', + type: 'parameter', + }, + ], +] as Provider[][]; + +const topLevelOnly = [ + [ + { + field: 'event.id', + value: 'kibana.alert.rule.uuid', + type: 'parameter', + }, + ], + [ + { + field: 'event.category', + value: 'network', + type: 'literal', + }, + ], + [ + { + field: 'process.pid', + value: 'process.pid', + type: 'parameter', + }, + ], +] as Provider[][]; + +const nonExistantField = [ + [ + { + field: 'event.id', + value: 'kibana.alert.rule.parameters.threshold.field', + type: 'parameter', + }, + ], +] as Provider[][]; + +describe('useInsightDataProviders', () => { + beforeEach(() => {}); + + it('should return 2 data providers, 1 with a nested provider ANDed to it', () => { + const { result } = renderHook(() => + useInsightDataProviders({ + providers: nestedAndProvider, + alertData: mockAlertDetailsDataWithIsObject, + }) + ); + const providers = result.current; + const providersWithNonEmptyAnd = providers.filter((provider) => provider.and.length > 0); + expect(providers.length).toBe(2); + expect(providersWithNonEmptyAnd.length).toBe(1); + }); + + it('should return 3 data providers without any containing nested ANDs', () => { + const { result } = renderHook(() => + useInsightDataProviders({ + providers: topLevelOnly, + alertData: mockAlertDetailsDataWithIsObject, + }) + ); + const providers = result.current; + const providersWithNonEmptyAnd = providers.filter((provider) => provider.and.length > 0); + expect(providers.length).toBe(3); + expect(providersWithNonEmptyAnd.length).toBe(0); + }); + + it('should use a wildcard for a field not present in an alert', () => { + const { result } = renderHook(() => + useInsightDataProviders({ + providers: nonExistantField, + alertData: mockAlertDetailsDataWithIsObject, + }) + ); + const providers = result.current; + const { + queryMatch: { value }, + } = providers[0]; + expect(providers.length).toBe(1); + expect(value).toBe('*'); + }); + + it('should use template data providers when called without alertData', () => { + const { result } = renderHook(() => + useInsightDataProviders({ + providers: nestedAndProvider, + }) + ); + const providers = result.current; + const [first, second] = providers; + const [nestedSecond] = second.and; + expect(second.type).toBe('default'); + expect(first.type).toBe('template'); + expect(nestedSecond.type).toBe('template'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts index c9abfbcd9972e..5c5de496b04b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.ts @@ -11,13 +11,20 @@ import { DataProviderType } from '@kbn/timelines-plugin/common'; import { IS_OPERATOR } from '../../../../../timelines/components/timeline/data_providers/data_provider'; import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; +export interface Provider { + field: string; + value: string; + type: 'parameter' | 'value'; +} +export interface UseInsightDataProvidersProps { + providers: Provider[][]; + alertData?: TimelineEventsDetailsItem[] | null; +} + export const useInsightDataProviders = ({ providers, alertData, -}: { - providers: Array>; - alertData?: TimelineEventsDetailsItem[] | null; -}): DataProvider[] => { +}: UseInsightDataProvidersProps): DataProvider[] => { function getFieldValue(fields: TimelineEventsDetailsItem[], fieldToFind: string) { const alertField = fields.find((dataField) => dataField.field === fieldToFind); return alertField?.values ? alertField.values[0] : '*'; From 9b32d751d63ee6293ee82ae7b40a39b1ccc8403a Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 15:21:03 +0000 Subject: [PATCH 10/21] Add locator --- .../components/markdown_editor/plugins/insight/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 1645de090dbaa..8eb38d9594bff 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -27,9 +27,9 @@ export const parser: Plugin = function () { const Parser = this.Parser; const tokenizers = Parser.prototype.inlineTokenizers; const methods = Parser.prototype.inlineMethods; + const insightPrefix = '!{insight'; const tokenizeInsight: RemarkTokenizer = function (eat, value, silent) { - const insightPrefix = '!{insight'; if (value.startsWith(insightPrefix) === false) { return false; } @@ -87,7 +87,9 @@ export const parser: Plugin = function () { } return false; }; - + tokenizeInsight.locator = (value: string, fromIndex: number) => { + return value.indexOf(insightPrefix, fromIndex); + }; tokenizers.insight = tokenizeInsight; methods.splice(methods.indexOf('text'), 0, 'insight'); }; From 89eb612138f4b7221bb3a156f806172103587ab9 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 15:40:56 +0000 Subject: [PATCH 11/21] Add sanity test for query hook --- .../use_insight_data_providers.test.ts | 2 - .../plugins/insight/use_insight_query.test.ts | 48 +++++++++++++++++++ .../plugins/insight/use_insight_query.ts | 10 ++-- 3 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts index 580968141994b..8542c445b5d14 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_data_providers.test.ts @@ -75,8 +75,6 @@ const nonExistantField = [ ] as Provider[][]; describe('useInsightDataProviders', () => { - beforeEach(() => {}); - it('should return 2 data providers, 1 with a nested provider ANDed to it', () => { const { result } = renderHook(() => useInsightDataProviders({ diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts new file mode 100644 index 0000000000000..57a23410dfb79 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import type { QueryOperator } from '@kbn/timelines-plugin/common'; +import { DataProviderType } from '@kbn/timelines-plugin/common'; +import { useInsightQuery } from './use_insight_query'; +import { TestProviders } from '../../../../mock'; +import type { UseInsightQuery, UseInsightQueryResult } from './use_insight_query'; +import { IS_OPERATOR } from '../../../../../timelines/components/timeline/data_providers/data_provider'; + +const mockProvider = { + and: [], + enabled: true, + id: 'made-up-id', + name: 'test', + excluded: false, + kqlQuery: '', + type: DataProviderType.default, + queryMatch: { + field: 'event.id', + value: '*', + operator: IS_OPERATOR as QueryOperator, + }, +}; + +describe('useInsightQuery', () => { + beforeEach(() => {}); + + it('should return renderable defaults', () => { + const { result } = renderHook( + () => + useInsightQuery({ + dataProviders: [mockProvider], + }), + { + wrapper: TestProviders, + } + ); + const { isQueryLoading, totalCount, oldestTimestamp } = result.current; + expect(isQueryLoading).toBeFalsy(); + expect(totalCount).toBe(-1); + expect(oldestTimestamp).toBe(undefined); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts index 15397f4594e6c..690ec5e1c5a89 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts @@ -15,17 +15,17 @@ import { useTimelineEvents } from '../../../../../timelines/containers'; import { useSourcererDataView } from '../../../../containers/sourcerer'; import { SourcererScopeName } from '../../../../store/sourcerer/model'; -interface UseInsightQuery { +export interface UseInsightQuery { dataProviders: DataProvider[]; } -export const useInsightQuery = ({ - dataProviders, -}: UseInsightQuery): { +export interface UseInsightQueryResult { isQueryLoading: boolean; totalCount: number; oldestTimestamp: string | null | undefined; -} => { +} + +export const useInsightQuery = ({ dataProviders }: UseInsightQuery): UseInsightQueryResult => { const { uiSettings } = useKibana().services; const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); const { browserFields, selectedPatterns, indexPattern, dataViewId } = useSourcererDataView( From cf46b22aeab944390c61f721d4c34557f2d226eb Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 16:00:23 +0000 Subject: [PATCH 12/21] Add sanity cypress test for insight markdown --- .../cypress/e2e/timelines/notes_tab.cy.ts | 8 ++++++++ .../plugins/security_solution/cypress/screens/timeline.ts | 2 ++ .../plugins/security_solution/cypress/tasks/timeline.ts | 2 +- .../components/markdown_editor/plugins/insight/index.tsx | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/e2e/timelines/notes_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/timelines/notes_tab.cy.ts index c20a4ae39026d..a6691225808ef 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/timelines/notes_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/timelines/notes_tab.cy.ts @@ -13,6 +13,7 @@ import { NOTES_LINK, NOTES_TEXT, NOTES_TEXT_AREA, + MARKDOWN_INVESTIGATE_BUTTON, } from '../../screens/timeline'; import { createTimeline } from '../../tasks/api_calls/timelines'; @@ -84,4 +85,11 @@ describe('Timeline notes tab', () => { cy.get(NOTES_LINK).last().should('have.text', `${text}(opens in a new tab or window)`); cy.get(NOTES_LINK).last().click(); }); + + it('should render insight query from markdown', () => { + addNotesToTimeline( + `!{insight{"description":"2 top level OR providers, 1 nested AND","label":"test insight", "providers": [[{ "field": "event.id", "value": "kibana.alert.original_event.id", "type": "parameter" }], [{ "field": "event.category", "value": "network", "type": "literal" }, {"field": "process.pid", "value": "process.pid", "type": "parameter"}]]}}` + ); + cy.get(MARKDOWN_INVESTIGATE_BUTTON).should('exist'); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 529847261e06d..77ccdfd8fae3b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -90,6 +90,8 @@ export const NOTES_AUTHOR = '.euiCommentEvent__headerUsername'; export const NOTES_LINK = '[data-test-subj="markdown-link"]'; +export const MARKDOWN_INVESTIGATE_BUTTON = '[data-test-subj="investigate-in-timeline-button"]'; + export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; export const OPEN_TIMELINE_MODAL = '[data-test-subj="open-timeline-modal"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 1ff04833db2a4..ec6782c07f735 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -161,7 +161,7 @@ export const addNotesToTimeline = (notes: string) => { .then(($el) => { const notesCount = parseInt($el.text(), 10); - cy.get(NOTES_TEXT_AREA).type(notes); + cy.get(NOTES_TEXT_AREA).type(notes, { parseSpecialCharSequences: false }); cy.get(ADD_NOTE_BUTTON).trigger('click'); cy.get(`${NOTES_TAB_BUTTON} .euiBadge`).should('have.text', `${notesCount + 1}`); }); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 8eb38d9594bff..5c78dbaff2ce1 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -126,6 +126,7 @@ const InsightComponent = ({ label, description, providers }: InsightComponentPro dataProviders={dataProviders} timeRange={oldestTimestamp} keepDataView={true} + data-test-subj="insight-investigate-in-timeline-button" > {` ${label} (${totalCount}) - ${description}`} From f716b8f1805d115ff02a5dddfae97e3199e0c4d2 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 17:46:35 +0000 Subject: [PATCH 13/21] Update cypress selector, add error message for invalid field --- .../cypress/screens/timeline.ts | 3 +- .../plugins/insight/use_insight_query.ts | 40 +++++++++++++------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 77ccdfd8fae3b..59c8d6a4103f7 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -90,7 +90,8 @@ export const NOTES_AUTHOR = '.euiCommentEvent__headerUsername'; export const NOTES_LINK = '[data-test-subj="markdown-link"]'; -export const MARKDOWN_INVESTIGATE_BUTTON = '[data-test-subj="investigate-in-timeline-button"]'; +export const MARKDOWN_INVESTIGATE_BUTTON = + '[data-test-subj="insight-investigate-in-timeline-button"]'; export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts index 690ec5e1c5a89..5bed77d0cde1f 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts @@ -6,11 +6,13 @@ */ import { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { DataProvider } from '@kbn/timelines-plugin/common'; import { useKibana } from '../../../../lib/kibana'; import { combineQueries } from '../../../../lib/kuery'; +import { useAppToasts } from '../../../../hooks/use_app_toasts'; import { useTimelineEvents } from '../../../../../timelines/containers'; import { useSourcererDataView } from '../../../../containers/sourcerer'; import { SourcererScopeName } from '../../../../store/sourcerer/model'; @@ -27,22 +29,36 @@ export interface UseInsightQueryResult { export const useInsightQuery = ({ dataProviders }: UseInsightQuery): UseInsightQueryResult => { const { uiSettings } = useKibana().services; + const { addError } = useAppToasts(); const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); const { browserFields, selectedPatterns, indexPattern, dataViewId } = useSourcererDataView( SourcererScopeName.timeline ); - const combinedQueries = combineQueries({ - config: esQueryConfig, - dataProviders, - indexPattern, - browserFields, - filters: [], - kqlQuery: { - query: '', - language: 'kuery', - }, - kqlMode: 'filter', - }); + let combinedQueries: { filterQuery: string | undefined; kqlError: Error | undefined } | null = + null; + try { + combinedQueries = combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters: [], + kqlQuery: { + query: '', + language: 'kuery', + }, + kqlMode: 'filter', + }); + } catch (err) { + addError(err, { + title: i18n.translate( + 'xpack.securitySolution.markdownEditor.plugins.insightProviderFieldError', + { + defaultMessage: 'Unable to parse insight provider configuration, invalid field name', + } + ), + }); + } const [isQueryLoading, { events, totalCount }] = useTimelineEvents({ dataViewId, fields: ['*'], From 7ca604999e9623940eb21fa6d2d169e1ea47c9d5 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 18:18:18 +0000 Subject: [PATCH 14/21] Disable button if combine queries fails --- .../table/investigate_in_timeline_button.tsx | 1 + .../markdown_editor/plugins/insight/index.tsx | 3 +- .../plugins/insight/use_insight_query.ts | 57 +++++++++---------- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx index 1f8a2129d5e18..044fbdf6a8f18 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx @@ -28,6 +28,7 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ filters?: Filter[] | null; timeRange?: string | null; keepDataView?: boolean; + isDisabled?: boolean; }> = ({ asEmptyButton, children, dataProviders, filters, timeRange, keepDataView, ...rest }) => { const dispatch = useDispatch(); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 5c78dbaff2ce1..78719bd83b067 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -114,7 +114,7 @@ const InsightComponent = ({ label, description, providers }: InsightComponentPro providers: parsedProviders, alertData, }); - const { totalCount, isQueryLoading, oldestTimestamp } = useInsightQuery({ + const { totalCount, isQueryLoading, oldestTimestamp, hasError } = useInsightQuery({ dataProviders, }); if (isQueryLoading) { @@ -123,6 +123,7 @@ const InsightComponent = ({ label, description, providers }: InsightComponentPro return ( { const { uiSettings } = useKibana().services; - const { addError } = useAppToasts(); const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]); const { browserFields, selectedPatterns, indexPattern, dataViewId } = useSourcererDataView( SourcererScopeName.timeline ); - let combinedQueries: { filterQuery: string | undefined; kqlError: Error | undefined } | null = - null; - try { - combinedQueries = combineQueries({ - config: esQueryConfig, - dataProviders, - indexPattern, - browserFields, - filters: [], - kqlQuery: { - query: '', - language: 'kuery', - }, - kqlMode: 'filter', - }); - } catch (err) { - addError(err, { - title: i18n.translate( - 'xpack.securitySolution.markdownEditor.plugins.insightProviderFieldError', - { - defaultMessage: 'Unable to parse insight provider configuration, invalid field name', - } - ), - }); - } + const [hasError, setHasError] = useState(false); + const [combinedQueries, setCombinedQueries] = useState<{ + filterQuery: string | undefined; + kqlError: Error | undefined; + } | null>(null); + useEffect(() => { + try { + const parsedCombinedQueries = combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters: [], + kqlQuery: { + query: '', + language: 'kuery', + }, + kqlMode: 'filter', + }); + setCombinedQueries(parsedCombinedQueries); + } catch (err) { + setHasError(true); + } + }, [browserFields, dataProviders, indexPattern, esQueryConfig]); const [isQueryLoading, { events, totalCount }] = useTimelineEvents({ dataViewId, fields: ['*'], @@ -77,5 +73,6 @@ export const useInsightQuery = ({ dataProviders }: UseInsightQuery): UseInsightQ isQueryLoading, totalCount, oldestTimestamp, + hasError, }; }; From 7cac44cd049527aab2604d85136e580f36d14dcf Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 19:07:12 +0000 Subject: [PATCH 15/21] Remove delay when using markdown editor, use better prop names --- .../security_solution/cypress/tasks/timeline.ts | 6 +++++- .../table/investigate_in_timeline_button.tsx | 9 +++------ .../markdown_editor/plugins/insight/index.tsx | 12 ++++++++++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index ec6782c07f735..76a512a6fcb88 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -161,7 +161,11 @@ export const addNotesToTimeline = (notes: string) => { .then(($el) => { const notesCount = parseInt($el.text(), 10); - cy.get(NOTES_TEXT_AREA).type(notes, { parseSpecialCharSequences: false }); + cy.get(NOTES_TEXT_AREA).type(notes, { + parseSpecialCharSequences: false, + delay: 0, + force: true, + }); cy.get(ADD_NOTE_BUTTON).trigger('click'); cy.get(`${NOTES_TAB_BUTTON} .euiBadge`).should('have.text', `${notesCount + 1}`); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx index 044fbdf6a8f18..3c095c5ed87da 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/investigate_in_timeline_button.tsx @@ -12,6 +12,7 @@ import { useDispatch } from 'react-redux'; import { sourcererSelectors } from '../../../store'; import { InputsModelId } from '../../../store/inputs/constants'; +import type { TimeRange } from '../../../store/inputs/model'; import { inputsActions } from '../../../store/inputs'; import { updateProviders, setFilters } from '../../../../timelines/store/timeline/actions'; import { sourcererActions } from '../../../store/actions'; @@ -26,7 +27,7 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ asEmptyButton: boolean; dataProviders: DataProvider[] | null; filters?: Filter[] | null; - timeRange?: string | null; + timeRange?: TimeRange; keepDataView?: boolean; isDisabled?: boolean; }> = ({ asEmptyButton, children, dataProviders, filters, timeRange, keepDataView, ...rest }) => { @@ -53,11 +54,7 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{ // Reset the current timeline if (timeRange) { clearTimeline({ - timeRange: { - kind: 'absolute', - from: timeRange, - to: new Date().toISOString(), - }, + timeRange, }); } else { clearTimeline(); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 78719bd83b067..e08d94b0f0782 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -6,7 +6,7 @@ */ import type { Plugin } from 'unified'; -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; import type { RemarkTokenizer } from '@elastic/eui'; import { EuiSpacer, EuiCodeBlock, EuiLoadingSpinner, EuiIcon } from '@elastic/eui'; import type { EuiMarkdownEditorUiPluginEditorProps } from '@elastic/eui/src/components/markdown_editor/markdown_types'; @@ -16,6 +16,7 @@ import { useInsightQuery } from './use_insight_query'; import { useInsightDataProviders } from './use_insight_data_providers'; import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; import { InvestigateInTimelineButton } from '../../../event_details/table/investigate_in_timeline_button'; +import type { AbsoluteTimeRange } from '../../../../store/inputs/model'; interface InsightComponentProps { label?: string; @@ -117,6 +118,13 @@ const InsightComponent = ({ label, description, providers }: InsightComponentPro const { totalCount, isQueryLoading, oldestTimestamp, hasError } = useInsightQuery({ dataProviders, }); + const timerange: AbsoluteTimeRange = useMemo(() => { + return { + kind: 'absolute', + from: oldestTimestamp ?? '', + to: new Date().toISOString(), + }; + }, [oldestTimestamp]); if (isQueryLoading) { return ; } else { @@ -125,7 +133,7 @@ const InsightComponent = ({ label, description, providers }: InsightComponentPro asEmptyButton={false} isDisabled={hasError} dataProviders={dataProviders} - timeRange={oldestTimestamp} + timeRange={timerange} keepDataView={true} data-test-subj="insight-investigate-in-timeline-button" > From 386ac1baad5a849946af05df35e446f80548da67 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 19:46:17 +0000 Subject: [PATCH 16/21] Fix types --- .../public/timelines/containers/index.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 4b7bf75da017d..816bcf12b981c 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -360,17 +360,22 @@ export const useTimelineEventsHandler = ({ ...deStructureEqlOptions(prevEqlRequest), }; + const requestTimeRange = + startDate && endDate + ? { + interval: '12h', + from: startDate, + to: endDate, + } + : {}; + const currentSearchParameters = { defaultIndex: indexNames, filterQuery: createFilter(filterQuery), querySize: limit, sort, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, runtimeMappings, + ...(requestTimeRange ? requestTimeRange : {}), ...deStructureEqlOptions(eqlOptions), }; @@ -391,11 +396,7 @@ export const useTimelineEventsHandler = ({ language, runtimeMappings, sort, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, + ...(requestTimeRange ? requestTimeRange : {}), ...(eqlOptions ? eqlOptions : {}), }; From 3ad85e392b0f5c91f5d84255911ab56f6be2fc1b Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 20:46:45 +0000 Subject: [PATCH 17/21] Fix types without breaking pagination --- .../security_solution/public/timelines/containers/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 816bcf12b981c..8cb938c14ab61 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -363,9 +363,7 @@ export const useTimelineEventsHandler = ({ const requestTimeRange = startDate && endDate ? { - interval: '12h', - from: startDate, - to: endDate, + timerange: { interval: '12h', from: startDate, to: endDate }, } : {}; From 710f4765af8227e3c6366a6900ecf5b71321b0dd Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 21:42:12 +0000 Subject: [PATCH 18/21] PR feedback --- .../markdown_editor/plugins/insight/index.tsx | 32 +------------------ .../plugins/insight/use_insight_query.ts | 3 +- .../public/timelines/containers/index.tsx | 9 ++++-- 3 files changed, 9 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index e08d94b0f0782..081ab17aa0dff 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -8,8 +8,7 @@ import type { Plugin } from 'unified'; import React, { useContext, useMemo } from 'react'; import type { RemarkTokenizer } from '@elastic/eui'; -import { EuiSpacer, EuiCodeBlock, EuiLoadingSpinner, EuiIcon } from '@elastic/eui'; -import type { EuiMarkdownEditorUiPluginEditorProps } from '@elastic/eui/src/components/markdown_editor/markdown_types'; +import { EuiLoadingSpinner, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useAppToasts } from '../../../../hooks/use_app_toasts'; import { useInsightQuery } from './use_insight_query'; @@ -145,32 +144,3 @@ const InsightComponent = ({ label, description, providers }: InsightComponentPro }; export { InsightComponent as renderer }; - -const InsightEditorComponent = ({ - node, - onSave, - onCancel, -}: EuiMarkdownEditorUiPluginEditorProps) => { - return ( -
- -
- ); -}; - -export const plugin = { - name: 'insight', - button: { - label: 'Insights', - iconType: 'timeline', - }, - helpText: ( -
- - {'!{insight{options}}'} - - -
- ), - editor: InsightEditorComponent, -}; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts index 9100c21883792..b4fa95cb1bdea 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts @@ -8,6 +8,7 @@ import { useMemo, useState, useEffect } from 'react'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { DataProvider } from '@kbn/timelines-plugin/common'; +import { TimelineId } from '../../../../../../common/types/timeline'; import { useKibana } from '../../../../lib/kibana'; import { combineQueries } from '../../../../lib/kuery'; import { useTimelineEvents } from '../../../../../timelines/containers'; @@ -59,7 +60,7 @@ export const useInsightQuery = ({ dataProviders }: UseInsightQuery): UseInsightQ dataViewId, fields: ['*'], filterQuery: combinedQueries?.filterQuery, - id: 'timeline-1', + id: TimelineId.active, indexNames: selectedPatterns, language: 'kuery', limit: 1, diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 8cb938c14ab61..4fa61144b22ec 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -366,14 +366,17 @@ export const useTimelineEventsHandler = ({ timerange: { interval: '12h', from: startDate, to: endDate }, } : {}; - + const timerange = + startDate && endDate + ? { timerange: { interval: '12h', from: startDate, to: endDate } } + : {}; const currentSearchParameters = { defaultIndex: indexNames, filterQuery: createFilter(filterQuery), querySize: limit, sort, runtimeMappings, - ...(requestTimeRange ? requestTimeRange : {}), + ...timerange, ...deStructureEqlOptions(eqlOptions), }; @@ -394,7 +397,7 @@ export const useTimelineEventsHandler = ({ language, runtimeMappings, sort, - ...(requestTimeRange ? requestTimeRange : {}), + ...timerange, ...(eqlOptions ? eqlOptions : {}), }; From be6cf77fa4155a8b18547dd35283d00200319498 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Tue, 15 Nov 2022 21:48:30 +0000 Subject: [PATCH 19/21] Remove unused variable --- .../security_solution/public/timelines/containers/index.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 4fa61144b22ec..55c8d2cad65bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -360,12 +360,6 @@ export const useTimelineEventsHandler = ({ ...deStructureEqlOptions(prevEqlRequest), }; - const requestTimeRange = - startDate && endDate - ? { - timerange: { interval: '12h', from: startDate, to: endDate }, - } - : {}; const timerange = startDate && endDate ? { timerange: { interval: '12h', from: startDate, to: endDate } } From 5558d77e91344dc9ac325dc894e64388ab423f4b Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Wed, 16 Nov 2022 03:32:53 +0000 Subject: [PATCH 20/21] Remove noop --- .../markdown_editor/plugins/insight/use_insight_query.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts index 57a23410dfb79..74942f0f4ad38 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.test.ts @@ -28,8 +28,6 @@ const mockProvider = { }; describe('useInsightQuery', () => { - beforeEach(() => {}); - it('should return renderable defaults', () => { const { result } = renderHook( () => From 8273e2f575fbb974a10db0c872232a45219539fb Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Wed, 16 Nov 2022 05:46:34 +0000 Subject: [PATCH 21/21] Prevent infinite loop with invalid markup in provider --- .../plugins/insight/use_insight_query.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts index b4fa95cb1bdea..e7836cd6cd3ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/use_insight_query.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useMemo, useState, useEffect } from 'react'; +import { useMemo, useState } from 'react'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { DataProvider } from '@kbn/timelines-plugin/common'; import { TimelineId } from '../../../../../../common/types/timeline'; @@ -33,29 +33,29 @@ export const useInsightQuery = ({ dataProviders }: UseInsightQuery): UseInsightQ SourcererScopeName.timeline ); const [hasError, setHasError] = useState(false); - const [combinedQueries, setCombinedQueries] = useState<{ - filterQuery: string | undefined; - kqlError: Error | undefined; - } | null>(null); - useEffect(() => { + const combinedQueries = useMemo(() => { try { - const parsedCombinedQueries = combineQueries({ - config: esQueryConfig, - dataProviders, - indexPattern, - browserFields, - filters: [], - kqlQuery: { - query: '', - language: 'kuery', - }, - kqlMode: 'filter', - }); - setCombinedQueries(parsedCombinedQueries); + if (hasError === false) { + const parsedCombinedQueries = combineQueries({ + config: esQueryConfig, + dataProviders, + indexPattern, + browserFields, + filters: [], + kqlQuery: { + query: '', + language: 'kuery', + }, + kqlMode: 'filter', + }); + return parsedCombinedQueries; + } } catch (err) { setHasError(true); + return null; } - }, [browserFields, dataProviders, indexPattern, esQueryConfig]); + }, [browserFields, dataProviders, esQueryConfig, hasError, indexPattern]); + const [isQueryLoading, { events, totalCount }] = useTimelineEvents({ dataViewId, fields: ['*'],