Skip to content

Commit

Permalink
[RAC][Security Solution] Alert table: Resolver and Cases icons to bul…
Browse files Browse the repository at this point in the history
…k action menu (#108420) (#108824)
  • Loading branch information
stephmilovic authored Aug 17, 2021
1 parent 6438e39 commit f8a03cf
Show file tree
Hide file tree
Showing 17 changed files with 344 additions and 212 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { login, loginAndWaitForPage, waitForPageWithoutDateRange } from '../../t
import { refreshPage } from '../../tasks/security_header';

import { ALERTS_URL } from '../../urls/navigation';
import { ATTACH_ALERT_TO_CASE_BUTTON } from '../../screens/alerts_detection_rules';
import { ATTACH_ALERT_TO_CASE_BUTTON, TIMELINE_CONTEXT_MENU_BTN } from '../../screens/alerts';

const loadDetectionsPage = (role: ROLES) => {
waitForPageWithoutDateRange(ALERTS_URL, role);
Expand Down Expand Up @@ -45,6 +45,7 @@ describe.skip('Alerts timeline', () => {
});

it('should not allow user with read only privileges to attach alerts to cases', () => {
cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click();
cy.get(ATTACH_ALERT_TO_CASE_BUTTON).should('not.exist');
});
});
Expand All @@ -55,6 +56,7 @@ describe.skip('Alerts timeline', () => {
});

it('should allow a user with crud privileges to attach alerts to cases', () => {
cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click();
cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('not.be.disabled');
});
});
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/cypress/screens/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,5 @@ export const SHOWING_ALERTS = '[data-test-subj="showingAlerts"]';
export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="selectedShowBulkActionsButton"]';

export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]';

export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="attach-alert-to-case-button"]';
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
* 2.0.
*/

export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="attach-alert-to-case-button"]';

export const BULK_ACTIONS_BTN = '[data-test-subj="bulkActions"] span';

export const CREATE_NEW_RULE_BTN = '[data-test-subj="create-new-rule"]';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { useKibana } from '../../lib/kibana';
import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
import { EventsViewer } from './events_viewer';
import * as i18n from './translations';

import { GraphOverlay } from '../../../timelines/components/graph_overlay';
const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = [];
const leadingControlColumns: ControlColumnProps[] = [
{
Expand Down Expand Up @@ -137,7 +137,13 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({

const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]);
const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS;

const graphOverlay = useMemo(
() =>
graphEventId != null && graphEventId.length > 0 ? (
<GraphOverlay isEventViewer={true} timelineId={id} />
) : null,
[graphEventId, id]
);
return (
<>
<FullScreenContainer $isFullScreen={globalFullScreen}>
Expand All @@ -155,6 +161,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
entityType,
filters: globalFilters,
globalFullScreen,
graphOverlay,
headerFilterGroup,
id,
indexNames: selectedPatterns,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ export const mockTimelines = {
.fn()
.mockReturnValue(<div data-test-subj="add-to-case-action">{'Add to case'}</div>),
getAddToCaseAction: jest.fn(),
getAddToExistingCaseButton: jest.fn(),
getAddToNewCaseButton: jest.fn(),
};
Original file line number Diff line number Diff line change
@@ -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 { mount } from 'enzyme';
import { AlertContextMenu } from './alert_context_menu';
import { TimelineId } from '../../../../../common';
import { TestProviders } from '../../../../common/mock';
import React from 'react';
import { Ecs } from '../../../../../common/ecs';
import { mockTimelines } from '../../../../common/mock/mock_timelines_plugin';

const ecsRowData: Ecs = { _id: '1', agent: { type: ['blah'] } };

const props = {
ariaLabel:
'Select more actions for the alert or event in row 26, with columns 2021-08-12T11:07:10.552Z Malware Prevention Alert high 73 siem-windows-endpoint SYSTEM powershell.exe mimikatz.exe ',
ariaRowindex: 26,
columnValues:
'2021-08-12T11:07:10.552Z Malware Prevention Alert high 73 siem-windows-endpoint SYSTEM powershell.exe mimikatz.exe ',
disabled: false,
ecsRowData,
refetch: jest.fn(),
timelineId: 'detections-page',
};

jest.mock('../../../../common/lib/kibana', () => ({
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
}),
useKibana: () => ({
services: {
timelines: { ...mockTimelines },
},
}),
useGetUserCasesPermissions: jest.fn().mockReturnValue({
crud: true,
read: true,
}),
}));

const actionMenuButton = '[data-test-subj="timeline-context-menu-button"] button';
const addToCaseButton = '[data-test-subj="attach-alert-to-case-button"]';

describe('InvestigateInResolverAction', () => {
test('it render AddToCase context menu item if timelineId === TimelineId.detectionsPage', () => {
const wrapper = mount(<AlertContextMenu {...props} timelineId={TimelineId.detectionsPage} />, {
wrappingComponent: TestProviders,
});

wrapper.find(actionMenuButton).simulate('click');
expect(wrapper.find(addToCaseButton).first().exists()).toEqual(true);
});

test('it render AddToCase context menu item if timelineId === TimelineId.detectionsRulesDetailsPage', () => {
const wrapper = mount(
<AlertContextMenu {...props} timelineId={TimelineId.detectionsRulesDetailsPage} />,
{
wrappingComponent: TestProviders,
}
);

wrapper.find(actionMenuButton).simulate('click');
expect(wrapper.find(addToCaseButton).first().exists()).toEqual(true);
});

test('it render AddToCase context menu item if timelineId === TimelineId.active', () => {
const wrapper = mount(<AlertContextMenu {...props} timelineId={TimelineId.active} />, {
wrappingComponent: TestProviders,
});

wrapper.find(actionMenuButton).simulate('click');
expect(wrapper.find(addToCaseButton).first().exists()).toEqual(true);
});

test('it does NOT render AddToCase context menu item when timelineId is not in the allowed list', () => {
const wrapper = mount(<AlertContextMenu {...props} timelineId="timeline-test" />, {
wrappingComponent: TestProviders,
});
wrapper.find(actionMenuButton).simulate('click');
expect(wrapper.find(addToCaseButton).first().exists()).toEqual(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@

import React, { useCallback, useMemo, useState } from 'react';

import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover, EuiToolTip } from '@elastic/eui';
import { EuiButtonIcon, EuiContextMenu, EuiPopover, EuiToolTip } from '@elastic/eui';
import { indexOf } from 'lodash';

import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
import { get, getOr } from 'lodash/fp';
import {
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
} from '@elastic/eui/src/components/context_menu/context_menu';
import styled from 'styled-components';
import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers';
import { EventsTdContent } from '../../../../timelines/components/timeline/styles';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers';
Expand All @@ -31,30 +36,92 @@ import { useExceptionModal } from './use_add_exception_modal';
import { useExceptionActions } from './use_add_exception_actions';
import { useEventFilterModal } from './use_event_filter_modal';
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
import { AddEventFilter } from './add_event_filter';
import { AddException } from './add_exception';
import { AddEndpointException } from './add_endpoint_exception';
import { useInsertTimeline } from '../../../../cases/components/use_insert_timeline';
import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
import { useInvestigateInResolverContextItem } from './investigate_in_resolver';
import { ATTACH_ALERT_TO_CASE_FOR_ROW } from '../../../../timelines/components/timeline/body/translations';
import { TimelineId } from '../../../../../common';
import { APP_ID } from '../../../../../common/constants';
import { useEventFilterAction } from './use_event_filter_action';

interface AlertContextMenuProps {
ariaLabel?: string;
ariaRowindex: number;
columnValues: string;
disabled: boolean;
ecsRowData: Ecs;
refetch: inputsModel.Refetch;
onRuleChange?: () => void;
timelineId: string;
}
export const NestedWrapper = styled.span`
button.euiContextMenuItem {
padding: 0;
}
`;

const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
ariaLabel = i18n.MORE_ACTIONS,
ariaRowindex,
columnValues,
disabled,
ecsRowData,
refetch,
onRuleChange,
timelineId,
}) => {
const [isPopoverOpen, setPopover] = useState(false);

const afterItemSelection = useCallback(() => {
setPopover(false);
}, []);
const ruleId = get(0, ecsRowData?.signal?.rule?.id);
const ruleName = get(0, ecsRowData?.signal?.rule?.name);
const { timelines: timelinesUi } = useKibana().services;
const casePermissions = useGetUserCasesPermissions();
const insertTimelineHook = useInsertTimeline;
const addToCaseActionProps = useMemo(
() => ({
ariaLabel: ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues }),
event: { data: [], ecs: ecsRowData, _id: ecsRowData._id },
useInsertTimeline: insertTimelineHook,
casePermissions,
appId: APP_ID,
onClose: afterItemSelection,
}),
[
ariaRowindex,
columnValues,
ecsRowData,
insertTimelineHook,
casePermissions,
afterItemSelection,
]
);
const hasWritePermissions = useGetUserCasesPermissions()?.crud ?? false;
const addToCaseAction = useMemo(
() =>
[
TimelineId.detectionsPage,
TimelineId.detectionsRulesDetailsPage,
TimelineId.active,
].includes(timelineId as TimelineId) && hasWritePermissions
? {
actionItem: [
{
name: i18n.ACTION_ADD_TO_CASE,
panel: 2,
'data-test-subj': 'attach-alert-to-case-button',
},
],
content: [
timelinesUi.getAddToExistingCaseButton(addToCaseActionProps),
timelinesUi.getAddToNewCaseButton(addToCaseActionProps),
],
}
: { actionItem: [], content: [] },
[addToCaseActionProps, hasWritePermissions, timelineId, timelinesUi]
);

const alertStatus = get(0, ecsRowData?.signal?.status) as Status;

Expand Down Expand Up @@ -133,45 +200,57 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
closePopover();
}, [closePopover, onAddEventFilterClick]);

const {
disabledAddEndpointException,
disabledAddException,
handleEndpointExceptionModal,
handleDetectionExceptionModal,
} = useExceptionActions({
const { exceptionActions } = useExceptionActions({
isEndpointAlert,
onAddExceptionTypeClick: handleOnAddExceptionTypeClick,
});

const items = useMemo(
const investigateInResolverAction = useInvestigateInResolverContextItem({
timelineId,
ecsData: ecsRowData,
onClose: afterItemSelection,
});
const eventFilterAction = useEventFilterAction({
onAddEventFilterClick: handleOnAddEventFilterClick,
});
const items: EuiContextMenuPanelItemDescriptor[] = useMemo(
() =>
!isEvent && ruleId
? [
...actionItems,
<AddEndpointException
onClick={handleEndpointExceptionModal}
disabled={disabledAddEndpointException}
/>,
<AddException
onClick={handleDetectionExceptionModal}
disabled={disabledAddException}
/>,
...investigateInResolverAction,
...addToCaseAction.actionItem,
...actionItems.map((aI) => ({ name: <NestedWrapper>{aI}</NestedWrapper> })),
...exceptionActions,
]
: [<AddEventFilter onClick={handleOnAddEventFilterClick} />],
: [...investigateInResolverAction, ...addToCaseAction.actionItem, eventFilterAction],
[
actionItems,
disabledAddEndpointException,
disabledAddException,
handleDetectionExceptionModal,
handleEndpointExceptionModal,
handleOnAddEventFilterClick,
addToCaseAction.actionItem,
eventFilterAction,
exceptionActions,
investigateInResolverAction,
isEvent,
ruleId,
]
);

const panels: EuiContextMenuPanelDescriptor[] = useMemo(
() => [
{
id: 0,
items,
},
{
id: 2,
title: i18n.ACTION_ADD_TO_CASE,
content: addToCaseAction.content,
},
],
[addToCaseAction.content, items]
);

return (
<>
{timelinesUi.getAddToCaseAction(addToCaseActionProps)}
<div key="actions-context-menu">
<EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
<EuiPopover
Expand All @@ -183,7 +262,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
anchorPosition="downLeft"
repositionOnScroll
>
<EuiContextMenuPanel size="s" items={items} />
<EuiContextMenu size="s" initialPanelId={0} panels={panels} />
</EuiPopover>
</EventsTdContent>
</div>
Expand Down
Loading

0 comments on commit f8a03cf

Please sign in to comment.