[Security Solution] Investigation guide - insights in markdown (#145240)

## Summary

This pr adds a new parsing plugin to the EuiMarkdownEditor used in
security solution that enables users to create run time queries that can
be parameterized from alert data, or hard coded literal values. A count
of the matching events is displayed in a button that when clicked will
open the same event set in timeline. Markdown is expected to be in the
following format:

`!{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"}]]}}`

The 2d array is used to allow nested queries, the top level arrays are
OR'ed together, and the inner array AND'ed together:
<img width="438" alt="image"
src="https://user-images.githubusercontent.com/56408403/201940553-96ab3d39-48fa-404f-ab2e-8946b532567b.png">


Following a prefix of !insight, the configuration object takes optional
description and label strings, along with a 2 dimensional array called
"providers". This value corresponds to what are called data providers in
the timeline view,

![image](https://user-images.githubusercontent.com/56408403/201936006-64e32d99-2764-4650-bd8b-da0a9420f8ed.png)


and are arrays of filters with 3 fields, "field" which is the field name
for that part of the query clause, "value" which is the value to be
used, and "type" which is either "parameter" or "literal". Filters of
type parameter expect value to be the name of a field present in an
alert document, and will use the value in the underlying document if
found. If the field is not present for some reason, a wildcard is used.
If the markdown is rendered in a context not tied to a specific alert,
parameter fields are treated as a timeline template field.
<img width="632" alt="image"
src="https://user-images.githubusercontent.com/56408403/201940922-7114a75f-0430-4397-8384-59f4e960ec9c.png">




### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
This commit is contained in:
Kevin Qualters 2022-11-16 10:17:40 -05:00 committed by GitHub
parent 58e6d9441a
commit 072c70dc99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 599 additions and 40 deletions

View file

@ -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');
});
});

View file

@ -90,6 +90,9 @@ export const NOTES_AUTHOR = '.euiCommentEvent__headerUsername';
export const NOTES_LINK = '[data-test-subj="markdown-link"]';
export const MARKDOWN_INVESTIGATE_BUTTON =
'[data-test-subj="insight-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"]';

View file

@ -161,7 +161,11 @@ 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,
delay: 0,
force: true,
});
cy.get(ADD_NOTE_BUTTON).trigger('click');
cy.get(`${NOTES_TAB_BUTTON} .euiBadge`).should('have.text', `${notesCount + 1}`);
});

View file

@ -5,13 +5,14 @@
* 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';
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,10 @@ export const InvestigateInTimelineButton: React.FunctionComponent<{
asEmptyButton: boolean;
dataProviders: DataProvider[] | null;
filters?: Filter[] | null;
}> = ({ asEmptyButton, children, dataProviders, filters, ...rest }) => {
timeRange?: TimeRange;
keepDataView?: boolean;
isDisabled?: boolean;
}> = ({ asEmptyButton, children, dataProviders, filters, timeRange, keepDataView, ...rest }) => {
const dispatch = useDispatch();
const getDataViewsSelector = useMemo(
@ -37,15 +41,24 @@ 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(() => {
const configureAndOpenTimeline = useCallback(() => {
if (dataProviders || filters) {
// Reset the current timeline
clearTimeline();
if (timeRange) {
clearTimeline({
timeRange,
});
} else {
clearTimeline();
}
if (dataProviders) {
// Update the timeline's providers to match the current prevalence field query
dispatch(
@ -66,17 +79,28 @@ 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]));
}
}, [dataProviders, clearTimeline, dispatch, defaultDataView.id, signalIndexName, filters]);
}, [
dataProviders,
clearTimeline,
dispatch,
defaultDataView.id,
signalIndexName,
filters,
timeRange,
keepDataView,
]);
return asEmptyButton ? (
<EuiButtonEmpty

View file

@ -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(),
@ -23,9 +24,11 @@ export const { uiPlugins, parsingPlugins, processingPlugins } = {
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;

View file

@ -0,0 +1,146 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Plugin } from 'unified';
import React, { useContext, useMemo } from 'react';
import type { RemarkTokenizer } from '@elastic/eui';
import { EuiLoadingSpinner, EuiIcon } from '@elastic/eui';
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';
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;
description?: string;
providers?: string;
}
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) {
if (value.startsWith(insightPrefix) === false) {
return false;
}
const nextChar = value[insightPrefix.length];
if (nextChar !== '{' && nextChar !== '}') return false;
if (silent) {
return true;
}
// is there a configuration?
const hasConfiguration = nextChar === '{';
let configuration: InsightComponentProps = {};
if (hasConfiguration) {
let configurationString = '';
let openObjects = 0;
for (let i = insightPrefix.length; 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;
}
}
try {
configuration = JSON.parse(configurationString);
return eat(value)({
type: 'insight',
...configuration,
providers: JSON.stringify(configuration.providers),
});
} catch (err) {
const now = eat.now();
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;
};
tokenizeInsight.locator = (value: string, fromIndex: number) => {
return value.indexOf(insightPrefix, fromIndex);
};
tokenizers.insight = tokenizeInsight;
methods.splice(methods.indexOf('text'), 0, 'insight');
};
// receives the configuration from the parser and renders
const InsightComponent = ({ label, description, providers }: InsightComponentProps) => {
const { addError } = useAppToasts();
let parsedProviders = [];
try {
if (providers !== undefined) {
parsedProviders = JSON.parse(providers);
}
} catch (err) {
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({
providers: parsedProviders,
alertData,
});
const { totalCount, isQueryLoading, oldestTimestamp, hasError } = useInsightQuery({
dataProviders,
});
const timerange: AbsoluteTimeRange = useMemo(() => {
return {
kind: 'absolute',
from: oldestTimestamp ?? '',
to: new Date().toISOString(),
};
}, [oldestTimestamp]);
if (isQueryLoading) {
return <EuiLoadingSpinner size="l" />;
} else {
return (
<InvestigateInTimelineButton
asEmptyButton={false}
isDisabled={hasError}
dataProviders={dataProviders}
timeRange={timerange}
keepDataView={true}
data-test-subj="insight-investigate-in-timeline-button"
>
<EuiIcon type="timeline" />
{` ${label} (${totalCount}) - ${description}`}
</InvestigateInTimelineButton>
);
}
};
export { InsightComponent as renderer };

View file

@ -0,0 +1,132 @@
/*
* 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', () => {
it('should return 2 data providers, 1 with a nested provider ANDed to it', () => {
const { result } = renderHook<UseInsightDataProvidersProps, DataProvider[]>(() =>
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<UseInsightDataProvidersProps, DataProvider[]>(() =>
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<UseInsightDataProvidersProps, DataProvider[]>(() =>
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<UseInsightDataProvidersProps, DataProvider[]>(() =>
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');
});
});

View file

@ -0,0 +1,114 @@
/*
* 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 } 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 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,
}: UseInsightDataProvidersProps): DataProvider[] => {
function getFieldValue(fields: TimelineEventsDetailsItem[], fieldToFind: string) {
const alertField = fields.find((dataField) => dataField.field === fieldToFind);
return alertField?.values ? alertField.values[0] : '*';
}
const dataProviders: DataProvider[] = useMemo(() => {
if (alertData) {
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 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;
};

View file

@ -0,0 +1,46 @@
/*
* 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', () => {
it('should return renderable defaults', () => {
const { result } = renderHook<UseInsightQuery, UseInsightQueryResult>(
() =>
useInsightQuery({
dataProviders: [mockProvider],
}),
{
wrapper: TestProviders,
}
);
const { isQueryLoading, totalCount, oldestTimestamp } = result.current;
expect(isQueryLoading).toBeFalsy();
expect(totalCount).toBe(-1);
expect(oldestTimestamp).toBe(undefined);
});
});

View file

@ -0,0 +1,79 @@
/*
* 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, useState } 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';
import { useSourcererDataView } from '../../../../containers/sourcerer';
import { SourcererScopeName } from '../../../../store/sourcerer/model';
export interface UseInsightQuery {
dataProviders: DataProvider[];
}
export interface UseInsightQueryResult {
isQueryLoading: boolean;
totalCount: number;
oldestTimestamp: string | null | undefined;
hasError: boolean;
}
export const useInsightQuery = ({ dataProviders }: UseInsightQuery): UseInsightQueryResult => {
const { uiSettings } = useKibana().services;
const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]);
const { browserFields, selectedPatterns, indexPattern, dataViewId } = useSourcererDataView(
SourcererScopeName.timeline
);
const [hasError, setHasError] = useState(false);
const combinedQueries = useMemo(() => {
try {
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, esQueryConfig, hasError, indexPattern]);
const [isQueryLoading, { events, totalCount }] = useTimelineEvents({
dataViewId,
fields: ['*'],
filterQuery: combinedQueries?.filterQuery,
id: TimelineId.active,
indexNames: selectedPatterns,
language: 'kuery',
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,
hasError,
};
};

View file

@ -258,6 +258,7 @@ const RunOsqueryButtonRenderer = ({
label?: string;
query: string;
ecs_mapping: { [key: string]: {} };
test: [];
};
}) => {
const [showFlyout, setShowFlyout] = useState(false);

View file

@ -24,7 +24,6 @@ const MarkdownRendererComponent: React.FC<Props> = ({ children, disableLinks })
() => (props) => <MarkdownLink {...props} disableLinks={disableLinks} />,
[disableLinks]
);
// 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.

View file

@ -19,6 +19,7 @@ export interface GetBasicDataFromDetailsData {
userName: string;
ruleName: string;
timestamp: string;
data: TimelineEventsDetailsItem[] | null;
}
export const useBasicDataFromDetailsData = (
@ -62,8 +63,9 @@ export const useBasicDataFromDetailsData = (
userName,
ruleName,
timestamp,
data,
}),
[agentId, alertId, hostName, isAlert, ruleName, timestamp, userName]
[agentId, alertId, hostName, isAlert, ruleName, timestamp, userName, data]
);
};

View file

@ -81,7 +81,7 @@ type TimelineResponse<T extends KueryFilterQueryKind> = T extends 'kuery'
export interface UseTimelineEventsProps {
dataViewId: string | null;
endDate: string;
endDate?: string;
eqlOptions?: EqlOptionsSelected;
fields: string[];
filterQuery?: ESQuery | string;
@ -92,7 +92,7 @@ export interface UseTimelineEventsProps {
runtimeMappings: MappingRuntimeFields;
skip?: boolean;
sort?: TimelineRequestSortField[];
startDate: string;
startDate?: string;
timerangeKind?: 'absolute' | 'relative';
}
@ -360,17 +360,17 @@ export const useTimelineEventsHandler = ({
...deStructureEqlOptions(prevEqlRequest),
};
const timerange =
startDate && endDate
? { timerange: { interval: '12h', from: startDate, to: endDate } }
: {};
const currentSearchParameters = {
defaultIndex: indexNames,
filterQuery: createFilter(filterQuery),
querySize: limit,
sort,
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
runtimeMappings,
...timerange,
...deStructureEqlOptions(eqlOptions),
};
@ -391,11 +391,7 @@ export const useTimelineEventsHandler = ({
language,
runtimeMappings,
sort,
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
...timerange,
...(eqlOptions ? eqlOptions : {}),
};

View file

@ -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;

View file

@ -27,17 +27,19 @@ export const buildEqlDsl = (options: TimelineEqlRequestOptions): Record<string,
throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`);
}
const requestFilter: unknown[] = [
{
range: {
[options.timestampField ?? '@timestamp']: {
gte: options.timerange.from,
lte: options.timerange.to,
format: 'strict_date_optional_time',
const requestFilter: unknown[] = options.timerange
? [
{
range: {
[options.timestampField ?? '@timestamp']: {
gte: options.timerange.from,
lte: options.timerange.to,
format: 'strict_date_optional_time',
},
},
},
},
},
];
]
: [];
return {
allow_no_indices: true,

View file

@ -38,6 +38,7 @@ export const timelineEventsAll: TimelineFactory<TimelineEventsQueries.all> = {
// 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;

View file

@ -28,7 +28,6 @@ export const buildTimelineEventsAllQuery = ({
timerange,
}: Omit<TimelineEventsAllRequestOptions, 'fieldRequested'>) => {
const filterClause = [...createQueryFilterClauses(filterQuery)];
const getTimerangeFilter = (timerangeOption: TimerangeInput | undefined): TimerangeFilter[] => {
if (timerangeOption) {
const { to, from } = timerangeOption;