[RAC][Security Solution] Alert table: Resolver and Cases icons to bulk action menu (#108420)

This commit is contained in:
Steph Milovic 2021-08-16 21:09:08 -06:00 committed by GitHub
parent 689d974729
commit 82af747532
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 344 additions and 212 deletions

View file

@ -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');
});
});

View file

@ -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"]';

View file

@ -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"]';

View file

@ -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,

View file

@ -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(),
};

View file

@ -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);
});
});

View file

@ -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>

View file

@ -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();
});
});
});

View file

@ -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,
},
];
};

View file

@ -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 {

View file

@ -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>
);
};

View file

@ -2,7 +2,7 @@
exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
<ColumnHeadersComponent
actionsColumnWidth={120}
actionsColumnWidth={72}
browserFields={
Object {
"agent": Object {

View file

@ -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

View file

@ -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

View file

@ -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';

View file

@ -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';

View file

@ -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