mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] Copies over settings from timeline template (#190511)
## Summary Handles : https://github.com/elastic/kibana/issues/189992 When user had created a timeline template and attached it to the rule, the columns were not being copied over from template to the timeline created from the alert generated by same rule. This PR fixes that as shown in demo below : https://github.com/user-attachments/assets/4237672e-943a-43f9-b160-5449399a5fd8 > [!Caution] > This PR checks below objects that are needed to be copied over from template > - columns > - data providers > > If we think, more things should be copied over, please comment below. ## Test Results  ### Checklist Delete any items that are not applicable to this PR. - [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
This commit is contained in:
parent
b336eecb2e
commit
a13f8d983c
3 changed files with 334 additions and 24 deletions
|
@ -1085,7 +1085,9 @@ export const sendAlertToTimelineAction = async ({
|
|||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(error);
|
||||
updateTimelineIsLoading({ id: TimelineId.active, isLoading: false });
|
||||
return createTimeline({
|
||||
from,
|
||||
|
|
|
@ -5,21 +5,30 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { of } from 'rxjs';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { KibanaServices, useKibana } from '../../../../common/lib/kibana';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import { useInvestigateInTimeline } from './use_investigate_in_timeline';
|
||||
import * as actions from '../actions';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import type { AlertTableContextMenuItem } from '../types';
|
||||
import React from 'react';
|
||||
import { EuiPopover, EuiContextMenu } from '@elastic/eui';
|
||||
import * as timelineActions from '../../../../timelines/store/actions';
|
||||
import { getTimelineTemplate } from '../../../../timelines/containers/api';
|
||||
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../timelines/containers/api');
|
||||
jest.mock('../../../../common/lib/apm/use_start_transaction');
|
||||
jest.mock('../../../../common/hooks/use_app_toasts');
|
||||
|
||||
const ecsRowData: Ecs = {
|
||||
_id: '1',
|
||||
agent: { type: ['blah'] },
|
||||
host: { name: ['some host name'] },
|
||||
kibana: {
|
||||
alert: {
|
||||
workflow_status: ['open'],
|
||||
|
@ -31,32 +40,207 @@ const ecsRowData: Ecs = {
|
|||
},
|
||||
};
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../common/lib/apm/use_start_transaction');
|
||||
jest.mock('../../../../common/hooks/use_app_toasts');
|
||||
jest.mock('../actions');
|
||||
|
||||
(KibanaServices.get as jest.Mock).mockReturnValue(coreMock.createStart());
|
||||
const mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction');
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
search: {
|
||||
searchStrategyClient: jest.fn(),
|
||||
},
|
||||
query: jest.fn(),
|
||||
},
|
||||
const nonECSRowData: TimelineEventsDetailsItem[] = [
|
||||
{
|
||||
category: 'agent',
|
||||
isObjectArray: false,
|
||||
field: 'agent.type',
|
||||
values: ['blah'],
|
||||
},
|
||||
});
|
||||
{
|
||||
category: 'kibana',
|
||||
isObjectArray: false,
|
||||
field: 'kibana.alert.workflow_status',
|
||||
values: ['open'],
|
||||
},
|
||||
{
|
||||
category: 'kibana',
|
||||
isObjectArray: false,
|
||||
field: 'kibana.alert.rule.uuid',
|
||||
values: ['testId'],
|
||||
},
|
||||
{
|
||||
category: 'host',
|
||||
isObjectArray: false,
|
||||
field: 'host.name',
|
||||
values: ['some host name'],
|
||||
},
|
||||
];
|
||||
|
||||
const getEcsDataWithRuleTypeAndTimelineTemplate = (ruleType: string, ecsData: Ecs = ecsRowData) => {
|
||||
return {
|
||||
...ecsData,
|
||||
kibana: {
|
||||
...(ecsData?.kibana ?? {}),
|
||||
alert: {
|
||||
...(ecsData.kibana?.alert ?? {}),
|
||||
rule: {
|
||||
...(ecsData.kibana?.alert.rule ?? {}),
|
||||
type: [ruleType],
|
||||
timeline_id: ['dummyTimelineTemplateId'],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Ecs;
|
||||
};
|
||||
|
||||
const getNonEcsDataWithRuleTypeAndTimelineTemplate = (
|
||||
ruleType: string,
|
||||
nonEcsData: TimelineEventsDetailsItem[] = nonECSRowData
|
||||
) => {
|
||||
return [
|
||||
...nonEcsData,
|
||||
{
|
||||
category: 'kibana',
|
||||
isObjectArray: false,
|
||||
field: 'kibana.alert.rule.type',
|
||||
values: [ruleType],
|
||||
},
|
||||
{
|
||||
category: 'kibana',
|
||||
isObjectArray: false,
|
||||
field: 'kibana.alert.rule.timeline_id',
|
||||
values: ['dummyTimelineTemplateId'],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction');
|
||||
|
||||
(useAppToasts as jest.Mock).mockReturnValue({
|
||||
addError: jest.fn(),
|
||||
});
|
||||
|
||||
const mockTimelineTemplateResponse = {
|
||||
data: {
|
||||
getOneTimeline: {
|
||||
savedObjectId: '15bc8185-06ef-4956-b7e7-be8e289b13c2',
|
||||
version: 'WzIzMzUsMl0=',
|
||||
columns: [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: '@timestamp',
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'host.name',
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'user.name',
|
||||
},
|
||||
],
|
||||
dataProviders: [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
id: 'some-random-id',
|
||||
name: 'host.name',
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: 'host.name',
|
||||
value: '{host.name}',
|
||||
operator: ':',
|
||||
},
|
||||
type: 'template',
|
||||
},
|
||||
],
|
||||
dataViewId: 'security-solution-default',
|
||||
description: '',
|
||||
eqlOptions: {
|
||||
eventCategoryField: 'event.category',
|
||||
tiebreakerField: '',
|
||||
timestampField: '@timestamp',
|
||||
query: '',
|
||||
size: 100,
|
||||
},
|
||||
eventType: 'all',
|
||||
excludedRowRendererIds: [
|
||||
'alert',
|
||||
'alerts',
|
||||
'auditd',
|
||||
'auditd_file',
|
||||
'library',
|
||||
'netflow',
|
||||
'plain',
|
||||
'registry',
|
||||
'suricata',
|
||||
'system',
|
||||
'system_dns',
|
||||
'system_endgame_process',
|
||||
'system_file',
|
||||
'system_fim',
|
||||
'system_security_event',
|
||||
'system_socket',
|
||||
'threat_match',
|
||||
'zeek',
|
||||
],
|
||||
favorite: [],
|
||||
filters: [],
|
||||
indexNames: ['.alerts-security.alerts-default', 'auditbeat-*', 'filebeat-*', 'packetbeat-*'],
|
||||
kqlMode: 'filter',
|
||||
kqlQuery: {
|
||||
filterQuery: {
|
||||
kuery: {
|
||||
kind: 'kuery',
|
||||
expression: '*',
|
||||
},
|
||||
serializedQuery: '{"query_string":{"query":"*"}}',
|
||||
},
|
||||
},
|
||||
title: 'Named Template',
|
||||
templateTimelineId: 'c755cda6-8a65-4ec2-b6ff-35a5356de8b9',
|
||||
templateTimelineVersion: 1,
|
||||
dateRange: {
|
||||
start: '2024-08-13T22:00:00.000Z',
|
||||
end: '2024-08-14T21:59:59.999Z',
|
||||
},
|
||||
savedQueryId: null,
|
||||
created: 1723625359467,
|
||||
createdBy: 'elastic',
|
||||
updated: 1723625359988,
|
||||
updatedBy: 'elastic',
|
||||
timelineType: 'template',
|
||||
status: 'active',
|
||||
sort: [
|
||||
{
|
||||
columnId: '@timestamp',
|
||||
columnType: 'date',
|
||||
sortDirection: 'desc',
|
||||
esTypes: ['date'],
|
||||
},
|
||||
],
|
||||
savedSearchId: null,
|
||||
eventIdToNoteIds: [],
|
||||
noteIds: [],
|
||||
notes: [],
|
||||
pinnedEventIds: [],
|
||||
pinnedEventsSaveObject: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const props = {
|
||||
ecsRowData,
|
||||
onInvestigateInTimelineAlertClick: () => {},
|
||||
onInvestigateInTimelineAlertClick: jest.fn(),
|
||||
};
|
||||
|
||||
const addTimelineSpy = jest.spyOn(timelineActions, 'addTimeline');
|
||||
|
||||
const RULE_TYPES_TO_BE_TESTED = [
|
||||
'query',
|
||||
'esql',
|
||||
'eql',
|
||||
'machine_learning',
|
||||
/* TODO: Complete test suites for below rule types */
|
||||
// 'new_terms',
|
||||
// 'eql',
|
||||
// 'threshold',
|
||||
// 'threat_match',
|
||||
];
|
||||
|
||||
const renderContextMenu = (items: AlertTableContextMenuItem[]) => {
|
||||
const panels = [{ id: 0, items }];
|
||||
return render(
|
||||
|
@ -72,11 +256,28 @@ const renderContextMenu = (items: AlertTableContextMenuItem[]) => {
|
|||
);
|
||||
};
|
||||
|
||||
describe('use investigate in timeline hook', () => {
|
||||
describe('useInvestigateInTimeline', () => {
|
||||
let mockSearchStrategyClient = {
|
||||
search: jest
|
||||
.fn()
|
||||
.mockReturnValue(of({ data: getNonEcsDataWithRuleTypeAndTimelineTemplate('query') })),
|
||||
};
|
||||
beforeEach(() => {
|
||||
(getTimelineTemplate as jest.Mock).mockResolvedValue(mockTimelineTemplateResponse);
|
||||
// by default we return data for query rule type
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
search: mockSearchStrategyClient,
|
||||
query: jest.fn(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('it creates a component and click handler', () => {
|
||||
test('creates a component and click handler', () => {
|
||||
const { result } = renderHook(() => useInvestigateInTimeline(props), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
@ -98,4 +299,101 @@ describe('use investigate in timeline hook', () => {
|
|||
expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('investigate an alert with timeline template', () => {
|
||||
describe.each(RULE_TYPES_TO_BE_TESTED)('Rule type : %s', (ruleType: string) => {
|
||||
test('should copy columns over from template', async () => {
|
||||
mockSearchStrategyClient = {
|
||||
search: jest
|
||||
.fn()
|
||||
.mockReturnValue(of({ data: getNonEcsDataWithRuleTypeAndTimelineTemplate(ruleType) })),
|
||||
};
|
||||
const ecsData = getEcsDataWithRuleTypeAndTimelineTemplate(ruleType);
|
||||
const { result } = renderHook(
|
||||
() => useInvestigateInTimeline({ ...props, ecsRowData: ecsData }),
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
const expectedColumns = [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: '@timestamp',
|
||||
type: 'date',
|
||||
initialWidth: 215,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'host.name',
|
||||
initialWidth: undefined,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'user.name',
|
||||
initialWidth: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const investigateAction = result.current.investigateInTimelineAlertClick;
|
||||
await investigateAction();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTimelineSpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
timeline: expect.objectContaining({
|
||||
columns: expectedColumns,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
test('should copy dataProviders over from template', async () => {
|
||||
mockSearchStrategyClient = {
|
||||
search: jest
|
||||
.fn()
|
||||
.mockReturnValue(of({ data: getNonEcsDataWithRuleTypeAndTimelineTemplate(ruleType) })),
|
||||
};
|
||||
const ecsData: Ecs = getEcsDataWithRuleTypeAndTimelineTemplate(ruleType);
|
||||
const { result } = renderHook(
|
||||
() => useInvestigateInTimeline({ ...props, ecsRowData: ecsData }),
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
const expectedDataProvider = [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
id: 'some-random-id',
|
||||
name: 'some host name',
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: 'host.name',
|
||||
value: 'some host name',
|
||||
operator: ':',
|
||||
},
|
||||
type: 'default',
|
||||
},
|
||||
];
|
||||
|
||||
const investigateAction = result.current.investigateInTimelineAlertClick;
|
||||
await investigateAction();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTimelineSpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
timeline: expect.objectContaining({
|
||||
dataProviders: expectedDataProvider,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useApi } from '@kbn/securitysolution-list-hooks';
|
|||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { createHistoryEntry } from '../../../../common/utils/global_query_string/helpers';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
@ -147,10 +148,19 @@ export const useInvestigateInTimeline = ({
|
|||
const unifiedComponentsInTimelineDisabled = useIsExperimentalFeatureEnabled(
|
||||
'unifiedComponentsInTimelineDisabled'
|
||||
);
|
||||
|
||||
const updateTimeline = useUpdateTimeline();
|
||||
|
||||
const createTimeline = useCallback(
|
||||
async ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => {
|
||||
const newColumns = timeline.columns;
|
||||
const newColumnsOverride =
|
||||
!newColumns || isEmpty(newColumns)
|
||||
? !unifiedComponentsInTimelineDisabled
|
||||
? defaultUdtHeaders
|
||||
: defaultHeaders
|
||||
: newColumns;
|
||||
|
||||
await clearActiveTimeline();
|
||||
updateTimelineIsLoading({ id: TimelineId.active, isLoading: false });
|
||||
updateTimeline({
|
||||
|
@ -160,7 +170,7 @@ export const useInvestigateInTimeline = ({
|
|||
notes: [],
|
||||
timeline: {
|
||||
...timeline,
|
||||
columns: !unifiedComponentsInTimelineDisabled ? defaultUdtHeaders : defaultHeaders,
|
||||
columns: newColumnsOverride,
|
||||
indexNames: timeline.indexNames ?? [],
|
||||
show: true,
|
||||
excludedRowRendererIds:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue