From 82af747532e9a4ef37204300b0fb5c385c4e090e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 16 Aug 2021 21:09:08 -0600 Subject: [PATCH] [RAC][Security Solution] Alert table: Resolver and Cases icons to bulk action menu (#108420) --- .../detection_alerts/attach_to_case.spec.ts | 4 +- .../cypress/screens/alerts.ts | 2 + .../cypress/screens/alerts_detection_rules.ts | 2 - .../common/components/events_viewer/index.tsx | 11 +- .../common/mock/mock_timelines_plugin.tsx | 2 + .../alert_context_menu.test.tsx | 88 ++++++++++++ .../timeline_actions/alert_context_menu.tsx | 135 ++++++++++++++---- .../investigate_in_resolver.test.tsx | 61 ++++++++ .../investigate_in_resolver.tsx | 52 +++++++ .../components/take_action_dropdown/index.tsx | 3 +- .../timeline/body/actions/index.tsx | 41 +----- .../__snapshots__/index.test.tsx.snap | 2 +- .../components/timeline/body/constants.ts | 6 +- .../body/events/event_column_view.test.tsx | 35 ----- .../components/timeline/body/helpers.test.ts | 51 ------- .../components/timeline/body/helpers.tsx | 53 +------ .../components/t_grid/integrated/index.tsx | 8 +- 17 files changed, 344 insertions(+), 212 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts index c81cf4277247..9ffade9abbb0 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts @@ -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); @@ -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'); }); }); @@ -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'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index 253da9b1c9ac..0d6787c49adb 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -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"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 9a23d98c1e91..e4e6a5610fdb 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -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"]'; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index c7b99f3b5a0b..108a744afc08 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -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[] = [ { @@ -137,7 +137,13 @@ const StatefulEventsViewerComponent: React.FC = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; - + const graphOverlay = useMemo( + () => + graphEventId != null && graphEventId.length > 0 ? ( + + ) : null, + [graphEventId, id] + ); return ( <> @@ -155,6 +161,7 @@ const StatefulEventsViewerComponent: React.FC = ({ entityType, filters: globalFilters, globalFullScreen, + graphOverlay, headerFilterGroup, id, indexNames: selectedPatterns, diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx b/x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx index efc2ce0fd6f4..3c597cff674a 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timelines_plugin.tsx @@ -19,4 +19,6 @@ export const mockTimelines = { .fn() .mockReturnValue(
{'Add to case'}
), getAddToCaseAction: jest.fn(), + getAddToExistingCaseButton: jest.fn(), + getAddToNewCaseButton: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx new file mode 100644 index 000000000000..b49c5602bc14 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -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(, { + 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( + , + { + 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(, { + 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(, { + wrappingComponent: TestProviders, + }); + wrapper.find(actionMenuButton).simulate('click'); + expect(wrapper.find(addToCaseButton).first().exists()).toEqual(false); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index ea451f424b43..9155d38ba315 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -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'; @@ -31,21 +36,34 @@ 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 = ({ ariaLabel = i18n.MORE_ACTIONS, + ariaRowindex, + columnValues, disabled, ecsRowData, refetch, @@ -53,8 +71,57 @@ const AlertContextMenuComponent: React.FC = ({ 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; @@ -133,45 +200,57 @@ const AlertContextMenuComponent: React.FC = ({ 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, - , - , + ...investigateInResolverAction, + ...addToCaseAction.actionItem, + ...actionItems.map((aI) => ({ name: {aI} })), + ...exceptionActions, ] - : [], + : [...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)}
= ({ anchorPosition="downLeft" repositionOnScroll > - +
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.test.tsx new file mode 100644 index 000000000000..f88dd37cb8ed --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.test.tsx @@ -0,0 +1,61 @@ +/* + * 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 { Ecs } from '../../../../../common/ecs'; +import { isInvestigateInResolverActionEnabled } from './investigate_in_resolver'; + +describe('InvestigateInResolverAction', () => { + describe('isInvestigateInResolverActionEnabled', () => { + it('returns false if agent.type does not equal endpoint', () => { + const data: Ecs = { _id: '1', agent: { type: ['blah'] } }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); + }); + + it('returns false if agent.type does not have endpoint in first array index', () => { + const data: Ecs = { _id: '1', agent: { type: ['blah', 'endpoint'] } }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); + }); + + it('returns false if process.entity_id is not defined', () => { + const data: Ecs = { _id: '1', agent: { type: ['endpoint'] } }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); + }); + + it('returns true if agent.type has endpoint in first array index', () => { + const data: Ecs = { + _id: '1', + agent: { type: ['endpoint', 'blah'] }, + process: { entity_id: ['5'] }, + }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeTruthy(); + }); + + it('returns false if multiple entity_ids', () => { + const data: Ecs = { + _id: '1', + agent: { type: ['endpoint', 'blah'] }, + process: { entity_id: ['5', '10'] }, + }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); + }); + + it('returns false if entity_id is an empty string', () => { + const data: Ecs = { + _id: '1', + agent: { type: ['endpoint', 'blah'] }, + process: { entity_id: [''] }, + }; + + expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx new file mode 100644 index 000000000000..52ae9684157a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx @@ -0,0 +1,52 @@ +/* + * 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 { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { get } from 'lodash/fp'; +import { + setActiveTabTimeline, + updateTimelineGraphEventId, +} from '../../../../timelines/store/timeline/actions'; +import { TimelineId, TimelineTabs } from '../../../../../common'; +import { ACTION_INVESTIGATE_IN_RESOLVER } from '../../../../timelines/components/timeline/body/translations'; +import { Ecs } from '../../../../../common/ecs'; + +export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => + (get(['agent', 'type', 0], ecsData) === 'endpoint' || + (get(['agent', 'type', 0], ecsData) === 'winlogbeat' && + get(['event', 'module', 0], ecsData) === 'sysmon')) && + get(['process', 'entity_id'], ecsData)?.length === 1 && + get(['process', 'entity_id', 0], ecsData) !== ''; +interface InvestigateInResolverProps { + timelineId: string; + ecsData: Ecs; + onClose: () => void; +} +export const useInvestigateInResolverContextItem = ({ + timelineId, + ecsData, + onClose, +}: InvestigateInResolverProps) => { + const dispatch = useDispatch(); + const isDisabled = useMemo(() => !isInvestigateInResolverActionEnabled(ecsData), [ecsData]); + const handleClick = useCallback(() => { + dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id })); + if (timelineId === TimelineId.active) { + dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph })); + } + onClose(); + }, [dispatch, ecsData._id, onClose, timelineId]); + return isDisabled + ? [] + : [ + { + name: ACTION_INVESTIGATE_IN_RESOLVER, + onClick: handleClick, + }, + ]; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index 077217b346a6..4487455c11a0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -26,6 +26,7 @@ import { getFieldValue } from '../host_isolation/helpers'; import type { Ecs } from '../../../../common/ecs'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { endpointAlertCheck } from '../../../common/utils/endpoint_alert_check'; +import { APP_ID } from '../../../../common/constants'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; interface ActionsData { @@ -187,7 +188,7 @@ export const TakeActionDropdown = React.memo( event: { data: [], ecs: ecsData, _id: ecsData._id }, useInsertTimeline: insertTimelineHook, casePermissions, - appId: 'securitySolution', + appId: APP_ID, onClose: afterCaseSelection, }; } else { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 942eeac9417c..c14973f91d8c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -12,23 +12,15 @@ import { noop } from 'lodash/fp'; import styled from 'styled-components'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; -import { - eventHasNotes, - getEventType, - getPinOnClick, - InvestigateInResolverAction, -} from '../helpers'; +import { eventHasNotes, getEventType, getPinOnClick } from '../helpers'; import { AlertContextMenu } from '../../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; import { InvestigateInTimelineAction } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; import { AddEventNoteAction } from '../actions/add_note_icon_item'; import { PinEventAction } from '../actions/pin_event_action'; import { EventsTdContent } from '../../styles'; -import { useKibana, useGetUserCasesPermissions } from '../../../../../common/lib/kibana'; -import { APP_ID } from '../../../../../../common/constants'; import * as i18n from '../translations'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; -import { useInsertTimeline } from '../../../../../cases/components/use_insert_timeline'; import { TimelineId, ActionProps, OnPinEvent } from '../../../../../../common/types/timeline'; import { timelineActions, timelineSelectors } from '../../../../store/timeline'; import { timelineDefaults } from '../../../../store/timeline/defaults'; @@ -63,7 +55,6 @@ const ActionsComponent: React.FC = ({ const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const emptyNotes: string[] = []; const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { timelines: timelinesUi } = useKibana().services; const onPinEvent: OnPinEvent = useCallback( (evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })), @@ -99,21 +90,12 @@ const ActionsComponent: React.FC = ({ (state) => (getTimeline(state, timelineId) ?? timelineDefaults).timelineType ); const eventType = getEventType(ecsData); - const casePermissions = useGetUserCasesPermissions(); - const insertTimelineHook = useInsertTimeline; + const isEventContextMenuEnabledForEndpoint = useMemo( () => ecsData.event?.kind?.includes('event') && ecsData.agent?.type?.includes('endpoint'), [ecsData.event?.kind, ecsData.agent?.type] ); - const addToCaseActionProps = useMemo(() => { - return { - ariaLabel: i18n.ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues }), - event: { data: [], ecs: ecsData, _id: ecsData._id }, - useInsertTimeline: insertTimelineHook, - casePermissions, - appId: APP_ID, - }; - }, [ariaRowindex, ecsData, casePermissions, insertTimelineHook, columnValues]); + return ( {showCheckboxes && !tGridEnabled && ( @@ -139,19 +121,13 @@ const ActionsComponent: React.FC = ({ <> - {timelineId !== TimelineId.active && eventType === 'signal' && ( = ({ /> )} - {[ - TimelineId.detectionsPage, - TimelineId.detectionsRulesDetailsPage, - TimelineId.active, - ].includes(timelineId as TimelineId) && - timelinesUi.getAddToCasePopover(addToCaseActionProps)} = ({ onRuleChange={onRuleChange} /> - {timelinesUi.getAddToCaseAction(addToCaseActionProps)} ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index b008f95285f2..6050263fff63 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -2,7 +2,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` { expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false); }); - test('it render AddToCaseAction if timelineId === TimelineId.detectionsPage', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy(); - }); - - test('it render AddToCaseAction if timelineId === TimelineId.detectionsRulesDetailsPage', () => { - const wrapper = mount( - , - { - wrappingComponent: TestProviders, - } - ); - - expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy(); - }); - - test('it render AddToCaseAction if timelineId === TimelineId.active', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy(); - }); - - test('it does NOT render AddToCaseAction when timelineId is not in the allowed list', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeFalsy(); - }); - test('it renders a custom control column in addition to the default control column', () => { const wrapper = mount( { }); }); - describe('isInvestigateInResolverActionEnabled', () => { - it('returns false if agent.type does not equal endpoint', () => { - const data: Ecs = { _id: '1', agent: { type: ['blah'] } }; - - expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); - }); - - it('returns false if agent.type does not have endpoint in first array index', () => { - const data: Ecs = { _id: '1', agent: { type: ['blah', 'endpoint'] } }; - - expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); - }); - - it('returns false if process.entity_id is not defined', () => { - const data: Ecs = { _id: '1', agent: { type: ['endpoint'] } }; - - expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); - }); - - it('returns true if agent.type has endpoint in first array index', () => { - const data: Ecs = { - _id: '1', - agent: { type: ['endpoint', 'blah'] }, - process: { entity_id: ['5'] }, - }; - - expect(isInvestigateInResolverActionEnabled(data)).toBeTruthy(); - }); - - it('returns false if multiple entity_ids', () => { - const data: Ecs = { - _id: '1', - agent: { type: ['endpoint', 'blah'] }, - process: { entity_id: ['5', '10'] }, - }; - - expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); - }); - - it('returns false if entity_id is an empty string', () => { - const data: Ecs = { - _id: '1', - agent: { type: ['endpoint', 'blah'] }, - process: { entity_id: [''] }, - }; - - expect(isInvestigateInResolverActionEnabled(data)).toBeFalsy(); - }); - }); - describe('getPinOnClick', () => { const eventId = 'abcd'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index dd701aa28499..8ddea99cddaa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -5,22 +5,16 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import { get, isEmpty } from 'lodash/fp'; -import { useDispatch } from 'react-redux'; +import { isEmpty } from 'lodash/fp'; import { Ecs } from '../../../../../common/ecs'; import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; -import { setActiveTabTimeline, updateTimelineGraphEventId } from '../../../store/timeline/actions'; import { TimelineEventsType, TimelineTypeLiteral, TimelineType, - TimelineId, - TimelineTabs, } from '../../../../../common/types/timeline'; import { OnPinEvent, OnUnPinEvent } from '../events'; -import { ActionIconItem } from './actions/action_icon_item'; import * as i18n from './translations'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -129,51 +123,6 @@ export const getEventType = (event: Ecs): Omit => { return 'raw'; }; -export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => - (get(['agent', 'type', 0], ecsData) === 'endpoint' || - (get(['agent', 'type', 0], ecsData) === 'winlogbeat' && - get(['event', 'module', 0], ecsData) === 'sysmon')) && - get(['process', 'entity_id'], ecsData)?.length === 1 && - get(['process', 'entity_id', 0], ecsData) !== ''; - -interface InvestigateInResolverActionProps { - ariaLabel?: string; - timelineId: string; - ecsData: Ecs; -} - -const InvestigateInResolverActionComponent: React.FC = ({ - ariaLabel = i18n.ACTION_INVESTIGATE_IN_RESOLVER, - timelineId, - ecsData, -}) => { - const dispatch = useDispatch(); - const isDisabled = useMemo(() => !isInvestigateInResolverActionEnabled(ecsData), [ecsData]); - const handleClick = useCallback(() => { - dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id })); - if (timelineId === TimelineId.active) { - dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph })); - } - }, [dispatch, ecsData._id, timelineId]); - - return ( - - ); -}; - -InvestigateInResolverActionComponent.displayName = 'InvestigateInResolverActionComponent'; - -export const InvestigateInResolverAction = React.memo(InvestigateInResolverActionComponent); - export const ROW_RENDERER_CLASS_NAME = 'row-renderer'; export const NOTES_CONTAINER_CLASS_NAME = 'notes-container'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 6b83039f3a54..23041a8f749c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -125,6 +125,7 @@ export interface TGridIntegratedProps { entityType: EntityType; filters: Filter[]; globalFullScreen: boolean; + graphOverlay?: React.ReactNode; headerFilterGroup?: React.ReactNode; filterStatus?: AlertStatus; height?: number; @@ -180,6 +181,7 @@ const TGridIntegratedComponent: React.FC = ({ start, sort, additionalFilters, + graphOverlay = null, graphEventId, leadingControlColumns, trailingControlColumns, @@ -332,13 +334,17 @@ const TGridIntegratedComponent: React.FC = ({ data-timeline-id={id} data-test-subj={`events-container-loading-${loading}`} > + {graphOverlay} {!resolverIsShowing(graphEventId) && additionalFilters} - + {nonDeletedEvents.length === 0 && loading === false ? (