Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SecuritySolution] Update timeline actions #155692

Merged
merged 5 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ import {
GLOBAL_SEARCH_BAR_FILTER_ITEM,
GLOBAL_SEARCH_BAR_FILTER_ITEM_DELETE,
} from '../../screens/search_bar';
import { TIMELINE_DATA_PROVIDERS_CONTAINER } from '../../screens/timeline';
import { closeTimelineUsingCloseButton } from '../../tasks/security_main';
import { TOASTER } from '../../screens/alerts_detection_rules';

describe('Histogram legend hover actions', { testIsolation: false }, () => {
const ruleConfigs = getNewRule();
Expand Down Expand Up @@ -60,8 +59,7 @@ describe('Histogram legend hover actions', { testIsolation: false }, () => {
it('Add To Timeline', function () {
clickAlertsHistogramLegend();
clickAlertsHistogramLegendAddToTimeline(ruleConfigs.name);
cy.get(TIMELINE_DATA_PROVIDERS_CONTAINER).should('be.visible');
cy.get(TIMELINE_DATA_PROVIDERS_CONTAINER).should('contain.text', getNewRule().name);
closeTimelineUsingCloseButton();

cy.get(TOASTER).should('have.text', `Added ${ruleConfigs.name} to timeline`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
*/

import type { SecurityAppStore } from '../../../common/store/types';
import type { DataProvider } from '../../../../common/types';
import { TimelineId } from '../../../../common/types';
import { addProvider } from '../../../timelines/store/timeline/actions';
import { createAddToNewTimelineCellActionFactory, getToastMessage } from './add_to_new_timeline';
import { addProvider, showTimeline } from '../../../timelines/store/timeline/actions';
import { createInvestigateInNewTimelineCellActionFactory } from './investigate_in_new_timeline';
import type { CellActionExecutionContext } from '@kbn/cell-actions';
import { GEO_FIELD_TYPE } from '../../../timelines/components/timeline/body/renderers/constants';
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
Expand Down Expand Up @@ -52,7 +51,7 @@ const defaultAddProviderAction = {
};

describe('createAddToNewTimelineCellAction', () => {
const addToTimelineCellActionFactory = createAddToNewTimelineCellActionFactory({
const addToTimelineCellActionFactory = createInvestigateInNewTimelineCellActionFactory({
store,
services,
});
Expand Down Expand Up @@ -175,73 +174,22 @@ describe('createAddToNewTimelineCellAction', () => {
},
});
});
});

describe('getToastMessage', () => {
it('handles empty input', () => {
const result = getToastMessage({ queryMatch: { value: null } } as unknown as DataProvider);
expect(result).toEqual('');
});
it('handles array input', () => {
const result = getToastMessage({
queryMatch: { value: ['hello', 'world'] },
} as unknown as DataProvider);
expect(result).toEqual('hello, world alerts');
});

it('handles single filter', () => {
const result = getToastMessage({
queryMatch: { value },
and: [{ queryMatch: { field: 'kibana.alert.severity', value: 'critical' } }],
} as unknown as DataProvider);
expect(result).toEqual(`critical severity alerts from ${value}`);
});

it('handles multiple filters', () => {
const result = getToastMessage({
queryMatch: { value },
and: [
{
queryMatch: { field: 'kibana.alert.workflow_status', value: 'open' },
},
{
queryMatch: { field: 'kibana.alert.severity', value: 'critical' },
},
],
} as unknown as DataProvider);
expect(result).toEqual(`open, critical severity alerts from ${value}`);
});

it('ignores unrelated filters', () => {
const result = getToastMessage({
queryMatch: { value },
and: [
{
queryMatch: { field: 'kibana.alert.workflow_status', value: 'open' },
},
{
queryMatch: { field: 'kibana.alert.severity', value: 'critical' },
},
// currently only supporting the above fields
{
queryMatch: { field: 'user.name', value: 'something' },
},
],
} as unknown as DataProvider);
expect(result).toEqual(`open, critical severity alerts from ${value}`);
});

it('returns entity only when unrelated filters are passed', () => {
const result = getToastMessage({
queryMatch: { value },
and: [{ queryMatch: { field: 'user.name', value: 'something' } }],
} as unknown as DataProvider);
expect(result).toEqual(`${value} alerts`);
});
it('should open the timeline', async () => {
await addToTimelineAction.execute({
...context,
metadata: {
andFilters: [{ field: 'kibana.alert.severity', value: 'low' }],
},
});

it('returns entity only when no filters are passed', () => {
const result = getToastMessage({ queryMatch: { value }, and: [] } as unknown as DataProvider);
expect(result).toEqual(`${value} alerts`);
expect(mockDispatch).toBeCalledWith({
type: showTimeline.type,
payload: {
id: TimelineId.active,
show: true,
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,23 @@

import { createCellActionFactory, type CellActionTemplate } from '@kbn/cell-actions';
import { timelineActions } from '../../../timelines/store/timeline';
import { addProvider } from '../../../timelines/store/timeline/actions';
import type { DataProvider } from '../../../../common/types';
import { addProvider, showTimeline } from '../../../timelines/store/timeline/actions';
import { TimelineId } from '../../../../common/types';
import type { SecurityAppStore } from '../../../common/store';
import { fieldHasCellActions } from '../../utils';
import {
ADD_TO_NEW_TIMELINE,
ADD_TO_TIMELINE_FAILED_TEXT,
ADD_TO_TIMELINE_FAILED_TITLE,
ADD_TO_TIMELINE_ICON,
ADD_TO_TIMELINE_SUCCESS_TITLE,
ALERTS_COUNT,
SEVERITY,
INVESTIGATE_IN_TIMELINE,
} from '../constants';
import { createDataProviders, isValidDataProviderField } from '../data_provider';
import { SecurityCellActionType } from '../../constants';
import type { StartServices } from '../../../types';
import type { SecurityCellAction } from '../../types';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';

const severityField = 'kibana.alert.severity';
const statusField = 'kibana.alert.workflow_status';

export const getToastMessage = ({ queryMatch: { value }, and = [] }: DataProvider) => {
if (value == null) {
return '';
}
const fieldValue = Array.isArray(value) ? value.join(', ') : value.toString();

const descriptors = and.reduce<string[]>((msg, { queryMatch }) => {
if (Array.isArray(queryMatch.value)) {
return msg;
}
if (queryMatch.field === severityField) {
msg.push(SEVERITY(queryMatch.value.toString()));
}
if (queryMatch.field === statusField) {
msg.push(queryMatch.value.toString());
}
return msg;
}, []);

return ALERTS_COUNT(fieldValue, descriptors.join(', '));
};

export const createAddToNewTimelineCellActionFactory = createCellActionFactory(
export const createInvestigateInNewTimelineCellActionFactory = createCellActionFactory(
({
store,
services,
Expand All @@ -63,10 +34,10 @@ export const createAddToNewTimelineCellActionFactory = createCellActionFactory(
const { notifications: notificationsService } = services;

return {
type: SecurityCellActionType.ADD_TO_TIMELINE,
type: SecurityCellActionType.INVESTIGATE_IN_NEW_TIMELINE,
getIconType: () => ADD_TO_TIMELINE_ICON,
getDisplayName: () => ADD_TO_NEW_TIMELINE,
getDisplayNameTooltip: () => ADD_TO_NEW_TIMELINE,
getDisplayName: () => INVESTIGATE_IN_TIMELINE,
getDisplayNameTooltip: () => INVESTIGATE_IN_TIMELINE,
isCompatible: async ({ field }) =>
fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type),
execute: async ({ field, metadata }) => {
Expand Down Expand Up @@ -101,10 +72,9 @@ export const createAddToNewTimelineCellActionFactory = createCellActionFactory(
id: TimelineId.active,
})
);
store.dispatch(showTimeline({ id: TimelineId.active, show: true }));

store.dispatch(addProvider({ id: TimelineId.active, providers: dataProviders }));
notificationsService.toasts.addSuccess({
title: ADD_TO_TIMELINE_SUCCESS_TITLE(getToastMessage(dataProviders[0])),
});
} else {
notificationsService.toasts.addWarning({
title: ADD_TO_TIMELINE_FAILED_TITLE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const ADD_TO_TIMELINE = i18n.translate(
defaultMessage: 'Add to timeline',
}
);
export const ADD_TO_NEW_TIMELINE = i18n.translate(
export const INVESTIGATE_IN_TIMELINE = i18n.translate(
'xpack.securitySolution.actions.cellValue.addToNewTimeline.displayName',
{
defaultMessage: 'Investigate in timeline',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
*/

export { createAddToTimelineCellActionFactory } from './cell_action/add_to_timeline';
export { createAddToNewTimelineCellActionFactory } from './cell_action/add_to_new_timeline';
export { createInvestigateInNewTimelineCellActionFactory } from './cell_action/investigate_in_new_timeline';
export { createAddToTimelineLensAction } from './lens/add_to_timeline';
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,26 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { Subject } from 'rxjs';
import type { CellValueContext, EmbeddableInput, IEmbeddable } from '@kbn/embeddable-plugin/public';
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
import { LENS_EMBEDDABLE_TYPE } from '@kbn/lens-plugin/public';
import type { SecurityAppStore } from '../../../common/store/types';
import { createAddToTimelineLensAction } from './add_to_timeline';
import { createAddToTimelineLensAction, getInvestigatedValue } from './add_to_timeline';
import { KibanaServices } from '../../../common/lib/kibana';
import { APP_UI_ID } from '../../../../common/constants';
import { Subject } from 'rxjs';
import { TimelineId } from '../../../../common/types';
import { addProvider, showTimeline } from '../../../timelines/store/timeline/actions';
import type { DataProvider } from '../../../../common/types';
import { TimelineId, EXISTS_OPERATOR } from '../../../../common/types';
import { addProvider } from '../../../timelines/store/timeline/actions';
import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';

jest.mock('../../../common/lib/kibana');
const currentAppId$ = new Subject<string | undefined>();
KibanaServices.get().application.currentAppId$ = currentAppId$.asObservable();
const mockWarningToast = jest.fn();
const mockSuccessToast = jest.fn();
KibanaServices.get().notifications.toasts.addWarning = mockWarningToast;

KibanaServices.get().notifications.toasts.addSuccess = mockSuccessToast;
const mockDispatch = jest.fn();
const store = {
dispatch: mockDispatch,
Expand Down Expand Up @@ -158,7 +159,7 @@ describe('createAddToTimelineLensAction', () => {
describe('execute', () => {
it('should execute normally', async () => {
await addToTimelineAction.execute(context);
expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(mockDispatch).toHaveBeenCalledWith({
type: addProvider.type,
payload: {
Expand All @@ -180,13 +181,8 @@ describe('createAddToTimelineLensAction', () => {
],
},
});
expect(mockDispatch).toHaveBeenCalledWith({
type: showTimeline.type,
payload: {
id: TimelineId.active,
show: true,
},
});

expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(mockWarningToast).not.toHaveBeenCalled();
});

Expand All @@ -195,7 +191,7 @@ describe('createAddToTimelineLensAction', () => {
...context,
data: [{ columnMeta }],
});
expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(mockDispatch).toHaveBeenCalledWith({
type: addProvider.type,
payload: {
Expand All @@ -217,13 +213,7 @@ describe('createAddToTimelineLensAction', () => {
],
},
});
expect(mockDispatch).toHaveBeenCalledWith({
type: showTimeline.type,
payload: {
id: TimelineId.active,
show: true,
},
});
expect(mockSuccessToast).toHaveBeenCalledTimes(1);
expect(mockWarningToast).not.toHaveBeenCalled();
});

Expand All @@ -242,7 +232,7 @@ describe('createAddToTimelineLensAction', () => {
},
],
});
expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(mockDispatch).toHaveBeenCalledWith({
type: addProvider.type,
payload: {
Expand All @@ -264,13 +254,8 @@ describe('createAddToTimelineLensAction', () => {
],
},
});
expect(mockDispatch).toHaveBeenCalledWith({
type: showTimeline.type,
payload: {
id: TimelineId.active,
show: true,
},
});

expect(mockSuccessToast).toHaveBeenCalledTimes(1);
expect(mockWarningToast).not.toHaveBeenCalled();
});

Expand All @@ -293,3 +278,28 @@ describe('createAddToTimelineLensAction', () => {
});
});
});

describe('getInvestigatedValue', () => {
it('handles empty input', () => {
const result = getInvestigatedValue([
{ queryMatch: { value: null } },
] as unknown as DataProvider[]);
expect(result).toEqual('');
});
it('handles array input', () => {
const result = getInvestigatedValue([
{
queryMatch: { value: ['hello', 'world'] },
},
] as unknown as DataProvider[]);
expect(result).toEqual('hello, world');
});
it('handles number value', () => {
const result = getInvestigatedValue([
{
queryMatch: { value: '', operator: EXISTS_OPERATOR, field: 'host.name' },
},
] as unknown as DataProvider[]);
expect(result).toEqual('host.name');
});
});
Loading