mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[RAC][Security Solution] Alert table: Resolver and Cases icons to bulk action menu (#108420)
This commit is contained in:
parent
689d974729
commit
82af747532
17 changed files with 344 additions and 212 deletions
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"]';
|
||||
|
|
|
@ -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"]';
|
||||
|
|
|
@ -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<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}>
|
||||
|
@ -155,6 +161,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
entityType,
|
||||
filters: globalFilters,
|
||||
globalFullScreen,
|
||||
graphOverlay,
|
||||
headerFilterGroup,
|
||||
id,
|
||||
indexNames: selectedPatterns,
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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<AlertContextMenuProps> = ({
|
||||
ariaLabel = i18n.MORE_ACTIONS,
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
disabled,
|
||||
ecsRowData,
|
||||
refetch,
|
||||
|
@ -53,8 +71,57 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
|
|||
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<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
|
||||
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
};
|
|
@ -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 {
|
||||
|
|
|
@ -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<ActionProps> = ({
|
|||
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<ActionProps> = ({
|
|||
(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 (
|
||||
<ActionsContainer>
|
||||
{showCheckboxes && !tGridEnabled && (
|
||||
|
@ -139,19 +121,13 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
<EuiButtonIcon
|
||||
aria-label={i18n.VIEW_DETAILS_FOR_ROW({ ariaRowindex, columnValues })}
|
||||
data-test-subj="expand-event"
|
||||
iconType="arrowRight"
|
||||
iconType="expand"
|
||||
onClick={onEventDetailsPanelOpened}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EventsTdContent>
|
||||
</div>
|
||||
<>
|
||||
<InvestigateInResolverAction
|
||||
ariaLabel={i18n.ACTION_INVESTIGATE_IN_RESOLVER_FOR_ROW({ ariaRowindex, columnValues })}
|
||||
key="investigate-in-resolver"
|
||||
timelineId={timelineId}
|
||||
ecsData={ecsData}
|
||||
/>
|
||||
{timelineId !== TimelineId.active && eventType === 'signal' && (
|
||||
<InvestigateInTimelineAction
|
||||
ariaLabel={i18n.SEND_ALERT_TO_TIMELINE_FOR_ROW({ ariaRowindex, columnValues })}
|
||||
|
@ -180,14 +156,10 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
{[
|
||||
TimelineId.detectionsPage,
|
||||
TimelineId.detectionsRulesDetailsPage,
|
||||
TimelineId.active,
|
||||
].includes(timelineId as TimelineId) &&
|
||||
timelinesUi.getAddToCasePopover(addToCaseActionProps)}
|
||||
<AlertContextMenu
|
||||
ariaLabel={i18n.MORE_ACTIONS_FOR_ROW({ ariaRowindex, columnValues })}
|
||||
ariaRowindex={ariaRowindex}
|
||||
columnValues={columnValues}
|
||||
key="alert-context-menu"
|
||||
ecsRowData={ecsData}
|
||||
timelineId={timelineId}
|
||||
|
@ -196,7 +168,6 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
onRuleChange={onRuleChange}
|
||||
/>
|
||||
</>
|
||||
{timelinesUi.getAddToCaseAction(addToCaseActionProps)}
|
||||
</ActionsContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
|
||||
<ColumnHeadersComponent
|
||||
actionsColumnWidth={120}
|
||||
actionsColumnWidth={72}
|
||||
browserFields={
|
||||
Object {
|
||||
"agent": Object {
|
||||
|
|
|
@ -6,18 +6,18 @@
|
|||
*/
|
||||
|
||||
/** The minimum (fixed) width of the Actions column */
|
||||
export const MINIMUM_ACTIONS_COLUMN_WIDTH = 148; // px;
|
||||
export const MINIMUM_ACTIONS_COLUMN_WIDTH = 100; // px;
|
||||
|
||||
/** Additional column width to include when checkboxes are shown **/
|
||||
export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px;
|
||||
|
||||
/** The (fixed) width of the Actions column */
|
||||
export const DEFAULT_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 5; // px;
|
||||
export const DEFAULT_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 3; // px;
|
||||
/**
|
||||
* The (fixed) width of the Actions column when the timeline body is used as
|
||||
* an events viewer, which has fewer actions than a regular events viewer
|
||||
*/
|
||||
export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 4; // px;
|
||||
export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 2; // px;
|
||||
|
||||
/** The default minimum width of a column (when a width for the column type is not specified) */
|
||||
export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px
|
||||
|
|
|
@ -134,41 +134,6 @@ describe('EventColumnView', () => {
|
|||
expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('it render AddToCaseAction if timelineId === TimelineId.detectionsPage', () => {
|
||||
const wrapper = mount(<EventColumnView {...props} timelineId={TimelineId.detectionsPage} />, {
|
||||
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(
|
||||
<EventColumnView {...props} timelineId={TimelineId.detectionsRulesDetailsPage} />,
|
||||
{
|
||||
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(<EventColumnView {...props} timelineId={TimelineId.active} />, {
|
||||
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(<EventColumnView {...props} timelineId="timeline-test" />, {
|
||||
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(
|
||||
<EventColumnView
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
getPinOnClick,
|
||||
getPinTooltip,
|
||||
stringifyEvent,
|
||||
isInvestigateInResolverActionEnabled,
|
||||
} from './helpers';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import { TimelineType } from '../../../../../common/types/timeline';
|
||||
|
@ -246,56 +245,6 @@ describe('helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
|
|
|
@ -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<TimelineEventsType, 'all'> => {
|
|||
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<InvestigateInResolverActionProps> = ({
|
||||
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 (
|
||||
<ActionIconItem
|
||||
ariaLabel={ariaLabel}
|
||||
content={
|
||||
isDisabled ? i18n.INVESTIGATE_IN_RESOLVER_DISABLED : i18n.ACTION_INVESTIGATE_IN_RESOLVER
|
||||
}
|
||||
dataTestSubj="investigate-in-resolver"
|
||||
iconType="analyzeEvent"
|
||||
isDisabled={isDisabled}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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';
|
||||
|
|
|
@ -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<TGridIntegratedProps> = ({
|
|||
start,
|
||||
sort,
|
||||
additionalFilters,
|
||||
graphOverlay = null,
|
||||
graphEventId,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
|
@ -332,13 +334,17 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
data-timeline-id={id}
|
||||
data-test-subj={`events-container-loading-${loading}`}
|
||||
>
|
||||
{graphOverlay}
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
|
||||
<UpdatedFlexItem grow={false} show={!loading}>
|
||||
{!resolverIsShowing(graphEventId) && additionalFilters}
|
||||
</UpdatedFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<FullWidthFlexGroup $visible={!graphEventId} gutterSize="none">
|
||||
<FullWidthFlexGroup
|
||||
$visible={!graphEventId && graphOverlay == null}
|
||||
gutterSize="none"
|
||||
>
|
||||
<ScrollableFlexItem grow={1}>
|
||||
{nonDeletedEvents.length === 0 && loading === false ? (
|
||||
<EuiEmptyPrompt
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue