mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] [Bugfix] Fixes broken alert actions (add to case, investigate in timeline) (#109339)
This commit is contained in:
parent
127100ffed
commit
a75db0550b
16 changed files with 1938 additions and 143 deletions
|
@ -22,6 +22,7 @@ import {
|
|||
import { FlowTarget } from '../../search_strategy/security_solution/network';
|
||||
import { errorSchema } from '../../detection_engine/schemas/response/error_schema';
|
||||
import { Direction, Maybe } from '../../search_strategy';
|
||||
import { Ecs } from '../../ecs';
|
||||
|
||||
export * from './actions';
|
||||
export * from './cells';
|
||||
|
@ -481,6 +482,7 @@ export type TimelineExpandedEventType =
|
|||
eventId: string;
|
||||
indexName: string;
|
||||
refetch?: () => void;
|
||||
ecsData?: Ecs;
|
||||
};
|
||||
}
|
||||
| EmptyObject;
|
||||
|
|
|
@ -31,13 +31,11 @@ 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 { useInsertTimeline } from '../../../../cases/components/use_insert_timeline';
|
||||
import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
|
||||
import { 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';
|
||||
import { useAddToCaseActions } from './use_add_to_case_actions';
|
||||
|
||||
interface AlertContextMenuProps {
|
||||
ariaLabel?: string;
|
||||
|
@ -68,41 +66,13 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
|
|||
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 addToCaseActionItems = useMemo(
|
||||
() =>
|
||||
[
|
||||
TimelineId.detectionsPage,
|
||||
TimelineId.detectionsRulesDetailsPage,
|
||||
TimelineId.active,
|
||||
].includes(timelineId as TimelineId) && hasWritePermissions
|
||||
? [
|
||||
timelinesUi.getAddToExistingCaseButton(addToCaseActionProps),
|
||||
timelinesUi.getAddToNewCaseButton(addToCaseActionProps),
|
||||
]
|
||||
: [],
|
||||
[addToCaseActionProps, hasWritePermissions, timelineId, timelinesUi]
|
||||
);
|
||||
|
||||
const { addToCaseActionProps, addToCaseActionItems } = useAddToCaseActions({
|
||||
ecsData: ecsRowData,
|
||||
afterCaseSelection: afterItemSelection,
|
||||
timelineId,
|
||||
ariaLabel: ATTACH_ALERT_TO_CASE_FOR_ROW({ ariaRowindex, columnValues }),
|
||||
});
|
||||
|
||||
const alertStatus = get(0, ecsRowData?.signal?.status) as Status;
|
||||
|
||||
|
@ -217,7 +187,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
{timelinesUi.getAddToCaseAction(addToCaseActionProps)}
|
||||
{addToCaseActionProps && timelinesUi.getAddToCaseAction(addToCaseActionProps)}
|
||||
{items.length > 0 && (
|
||||
<div key="actions-context-menu">
|
||||
<EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
|
||||
import { TimelineId, TimelineNonEcsData } from '../../../../../common';
|
||||
import { APP_ID } from '../../../../../common/constants';
|
||||
import { useInsertTimeline } from '../../../../cases/components/use_insert_timeline';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
|
||||
export interface UseAddToCaseActions {
|
||||
afterCaseSelection: () => void;
|
||||
ariaLabel?: string;
|
||||
ecsData?: Ecs;
|
||||
nonEcsData?: TimelineNonEcsData[];
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
export const useAddToCaseActions = ({
|
||||
afterCaseSelection,
|
||||
ariaLabel,
|
||||
ecsData,
|
||||
nonEcsData,
|
||||
timelineId,
|
||||
}: UseAddToCaseActions) => {
|
||||
const { timelines: timelinesUi } = useKibana().services;
|
||||
const casePermissions = useGetUserCasesPermissions();
|
||||
const insertTimelineHook = useInsertTimeline;
|
||||
|
||||
const addToCaseActionProps = useMemo(
|
||||
() =>
|
||||
ecsData?._id
|
||||
? {
|
||||
ariaLabel,
|
||||
event: { data: nonEcsData ?? [], ecs: ecsData, _id: ecsData?._id },
|
||||
useInsertTimeline: insertTimelineHook,
|
||||
casePermissions,
|
||||
appId: APP_ID,
|
||||
onClose: afterCaseSelection,
|
||||
}
|
||||
: null,
|
||||
[ecsData, ariaLabel, nonEcsData, insertTimelineHook, casePermissions, afterCaseSelection]
|
||||
);
|
||||
const hasWritePermissions = casePermissions?.crud ?? false;
|
||||
const addToCaseActionItems = useMemo(
|
||||
() =>
|
||||
[
|
||||
TimelineId.detectionsPage,
|
||||
TimelineId.detectionsRulesDetailsPage,
|
||||
TimelineId.active,
|
||||
].includes(timelineId as TimelineId) &&
|
||||
hasWritePermissions &&
|
||||
addToCaseActionProps
|
||||
? [
|
||||
timelinesUi.getAddToExistingCaseButton(addToCaseActionProps),
|
||||
timelinesUi.getAddToNewCaseButton(addToCaseActionProps),
|
||||
]
|
||||
: [],
|
||||
[addToCaseActionProps, hasWritePermissions, timelineId, timelinesUi]
|
||||
);
|
||||
|
||||
return {
|
||||
addToCaseActionItems,
|
||||
addToCaseActionProps,
|
||||
};
|
||||
};
|
|
@ -108,7 +108,7 @@ export const useInvestigateInTimeline = ({
|
|||
<EuiContextMenuItem
|
||||
key="investigate-in-timeline-action-item"
|
||||
data-test-subj="investigate-in-timeline-action-item"
|
||||
disabled={isFetchingAlertEcs === true}
|
||||
disabled={ecsRowData == null && isFetchingAlertEcs === true}
|
||||
onClick={investigateInTimelineAlertClick}
|
||||
>
|
||||
{ACTION_INVESTIGATE_IN_TIMELINE}
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { TakeActionDropdown, TakeActionDropdownProps } from '.';
|
||||
import { mockAlertDetailsData } from '../../../common/components/event_details/__mocks__';
|
||||
import { mockEcsDataWithAlert } from '../../../common/mock/mock_detection_alerts';
|
||||
import { TimelineEventsDetailsItem, TimelineId } from '../../../../common';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { mockTimelines } from '../../../common/mock/mock_timelines_plugin';
|
||||
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
jest.mock('../../../common/hooks/endpoint/use_isolate_privileges', () => ({
|
||||
useIsolationPrivileges: jest.fn().mockReturnValue({ isAllowed: true }),
|
||||
}));
|
||||
jest.mock('../../../common/lib/kibana', () => ({
|
||||
useKibana: jest.fn(),
|
||||
useGetUserCasesPermissions: jest.fn().mockReturnValue({ crud: true }),
|
||||
}));
|
||||
jest.mock('../../../cases/components/use_insert_timeline');
|
||||
|
||||
jest.mock('../../../common/hooks/use_experimental_features', () => ({
|
||||
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
jest.mock('@kbn/alerts', () => {
|
||||
return { useGetUserAlertsPermissions: jest.fn().mockReturnValue({ crud: true }) };
|
||||
});
|
||||
|
||||
jest.mock('../../../common/utils/endpoint_alert_check', () => {
|
||||
return { endpointAlertCheck: jest.fn().mockReturnValue(true) };
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/endpoint/service/host_isolation/utils', () => {
|
||||
return {
|
||||
isIsolationSupported: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../containers/detection_engine/alerts/use_host_isolation_status', () => {
|
||||
return {
|
||||
useHostIsolationStatus: jest.fn().mockReturnValue({
|
||||
loading: false,
|
||||
isIsolated: false,
|
||||
agentStatus: 'healthy',
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('take action dropdown', () => {
|
||||
const defaultProps: TakeActionDropdownProps = {
|
||||
detailsData: mockAlertDetailsData as TimelineEventsDetailsItem[],
|
||||
ecsData: mockEcsDataWithAlert,
|
||||
handleOnEventClosed: jest.fn(),
|
||||
indexName: 'index',
|
||||
isHostIsolationPanelOpen: false,
|
||||
loadingEventDetails: false,
|
||||
onAddEventFilterClick: jest.fn(),
|
||||
onAddExceptionTypeClick: jest.fn(),
|
||||
onAddIsolationStatusClick: jest.fn(),
|
||||
refetch: jest.fn(),
|
||||
timelineId: TimelineId.active,
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
(useKibana as jest.Mock).mockImplementation(() => {
|
||||
const mockStartServicesMock = createStartServicesMock();
|
||||
|
||||
return {
|
||||
services: {
|
||||
...mockStartServicesMock,
|
||||
timelines: { ...mockTimelines },
|
||||
application: {
|
||||
capabilities: { siem: { crud_alerts: true, read_alerts: true } },
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
test('should render takeActionButton', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<TakeActionDropdown {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="take-action-dropdown-btn"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should render takeActionButton with correct text', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<TakeActionDropdown {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="take-action-dropdown-btn"]').first().text()).toEqual(
|
||||
'Take action'
|
||||
);
|
||||
});
|
||||
|
||||
describe('should render take action items', () => {
|
||||
const testProps = {
|
||||
...defaultProps,
|
||||
};
|
||||
let wrapper: ReactWrapper;
|
||||
beforeAll(() => {
|
||||
wrapper = mount(
|
||||
<TestProviders>
|
||||
<TakeActionDropdown {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('button[data-test-subj="take-action-dropdown-btn"]').simulate('click');
|
||||
});
|
||||
test('should render "Add to existing case"', async () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="add-to-existing-case-action"]').first().text()
|
||||
).toEqual('Add to existing case');
|
||||
});
|
||||
});
|
||||
test('should render "Add to new case"', async () => {
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="add-to-new-case-action"]').first().text()).toEqual(
|
||||
'Add to new case'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should render "mark as acknowledge"', async () => {
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="acknowledged-alert-status"]').first().text()).toEqual(
|
||||
'Mark as acknowledged'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should render "mark as close"', async () => {
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="close-alert-status"]').first().text()).toEqual(
|
||||
'Mark as closed'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should render "Add Endpoint exception"', async () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="add-endpoint-exception-menu-item"]').first().text()
|
||||
).toEqual('Add Endpoint exception');
|
||||
});
|
||||
});
|
||||
test('should render "Add rule exception"', async () => {
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="add-exception-menu-item"]').first().text()).toEqual(
|
||||
'Add rule exception'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should render "Isolate host"', async () => {
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="isolate-host-action-item"]').first().text()).toEqual(
|
||||
'Isolate host'
|
||||
);
|
||||
});
|
||||
});
|
||||
test('should render "Investigate in timeline"', async () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="investigate-in-timeline-action-item"]').first().text()
|
||||
).toEqual('Investigate in timeline');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,23 +8,21 @@
|
|||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui';
|
||||
import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { TimelineEventsDetailsItem } from '../../../../common';
|
||||
import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations';
|
||||
|
||||
import { TimelineEventsDetailsItem, TimelineNonEcsData } from '../../../../common';
|
||||
import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions';
|
||||
import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions';
|
||||
import { useInvestigateInTimeline } from '../alerts_table/timeline_actions/use_investigate_in_timeline';
|
||||
import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana';
|
||||
import { useInsertTimeline } from '../../../cases/components/use_insert_timeline';
|
||||
|
||||
import { useEventFilterAction } from '../alerts_table/timeline_actions/use_event_filter_action';
|
||||
import { useHostIsolationAction } from '../host_isolation/use_host_isolation_action';
|
||||
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';
|
||||
import { useAddToCaseActions } from '../alerts_table/timeline_actions/use_add_to_case_actions';
|
||||
|
||||
interface ActionsData {
|
||||
alertStatus: Status;
|
||||
|
@ -34,39 +32,36 @@ interface ActionsData {
|
|||
ruleName: string;
|
||||
}
|
||||
|
||||
export interface TakeActionDropdownProps {
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
ecsData?: Ecs;
|
||||
handleOnEventClosed: () => void;
|
||||
indexName: string;
|
||||
isHostIsolationPanelOpen: boolean;
|
||||
loadingEventDetails: boolean;
|
||||
onAddEventFilterClick: () => void;
|
||||
onAddExceptionTypeClick: (type: ExceptionListType) => void;
|
||||
onAddIsolationStatusClick: (action: 'isolateHost' | 'unisolateHost') => void;
|
||||
refetch: (() => void) | undefined;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
export const TakeActionDropdown = React.memo(
|
||||
({
|
||||
detailsData,
|
||||
ecsData,
|
||||
handleOnEventClosed,
|
||||
indexName,
|
||||
isHostIsolationPanelOpen,
|
||||
loadingEventDetails,
|
||||
nonEcsData,
|
||||
onAddEventFilterClick,
|
||||
onAddExceptionTypeClick,
|
||||
onAddIsolationStatusClick,
|
||||
refetch,
|
||||
indexName,
|
||||
timelineId,
|
||||
}: {
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
ecsData?: Ecs;
|
||||
handleOnEventClosed: () => void;
|
||||
isHostIsolationPanelOpen: boolean;
|
||||
loadingEventDetails: boolean;
|
||||
nonEcsData?: TimelineNonEcsData[];
|
||||
refetch: (() => void) | undefined;
|
||||
indexName: string;
|
||||
onAddEventFilterClick: () => void;
|
||||
onAddExceptionTypeClick: (type: ExceptionListType) => void;
|
||||
onAddIsolationStatusClick: (action: 'isolateHost' | 'unisolateHost') => void;
|
||||
timelineId: string;
|
||||
}) => {
|
||||
const casePermissions = useGetUserCasesPermissions();
|
||||
}: TakeActionDropdownProps) => {
|
||||
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
|
||||
|
||||
const { timelines: timelinesUi } = useKibana().services;
|
||||
const insertTimelineHook = useInsertTimeline;
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const actionsData = useMemo(
|
||||
|
@ -87,7 +82,9 @@ export const TakeActionDropdown = React.memo(
|
|||
[detailsData]
|
||||
);
|
||||
|
||||
const alertIds = useMemo(() => [actionsData.eventId], [actionsData.eventId]);
|
||||
const alertIds = useMemo(() => (isEmpty(actionsData.eventId) ? null : [actionsData.eventId]), [
|
||||
actionsData.eventId,
|
||||
]);
|
||||
const isEvent = actionsData.eventKind === 'event';
|
||||
|
||||
const isEndpointAlert = useMemo((): boolean => {
|
||||
|
@ -153,11 +150,11 @@ export const TakeActionDropdown = React.memo(
|
|||
|
||||
const { actionItems: statusActionItems } = useAlertsActions({
|
||||
alertStatus: actionsData.alertStatus,
|
||||
closePopover: closePopoverAndFlyout,
|
||||
eventId: actionsData.eventId,
|
||||
indexName,
|
||||
timelineId,
|
||||
refetch,
|
||||
closePopover: closePopoverAndFlyout,
|
||||
timelineId,
|
||||
});
|
||||
|
||||
const { investigateInTimelineActionItems } = useInvestigateInTimeline({
|
||||
|
@ -174,37 +171,16 @@ export const TakeActionDropdown = React.memo(
|
|||
[eventFilterActionItems, exceptionActionItems, statusActionItems, isEvent, actionsData.ruleId]
|
||||
);
|
||||
|
||||
const addToCaseProps = useMemo(() => {
|
||||
if (ecsData) {
|
||||
return {
|
||||
event: { data: [], ecs: ecsData, _id: ecsData._id },
|
||||
useInsertTimeline: insertTimelineHook,
|
||||
casePermissions,
|
||||
appId: APP_ID,
|
||||
onClose: afterCaseSelection,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [afterCaseSelection, casePermissions, ecsData, insertTimelineHook]);
|
||||
|
||||
const addToCasesActionItems = useMemo(
|
||||
() =>
|
||||
addToCaseProps &&
|
||||
['detections-page', 'detections-rules-details-page', 'timeline-1'].includes(
|
||||
timelineId ?? ''
|
||||
)
|
||||
? [
|
||||
timelinesUi.getAddToExistingCaseButton(addToCaseProps),
|
||||
timelinesUi.getAddToNewCaseButton(addToCaseProps),
|
||||
]
|
||||
: [],
|
||||
[timelinesUi, addToCaseProps, timelineId]
|
||||
);
|
||||
const { addToCaseActionItems } = useAddToCaseActions({
|
||||
ecsData,
|
||||
nonEcsData: detailsData?.map((d) => ({ field: d.field, value: d.values })) ?? [],
|
||||
afterCaseSelection,
|
||||
timelineId,
|
||||
});
|
||||
|
||||
const items: React.ReactElement[] = useMemo(
|
||||
() => [
|
||||
...(tGridEnabled ? addToCasesActionItems : []),
|
||||
...(tGridEnabled ? addToCaseActionItems : []),
|
||||
...alertsActionItems,
|
||||
...hostIsolationActionItems,
|
||||
...investigateInTimelineActionItems,
|
||||
|
@ -212,7 +188,7 @@ export const TakeActionDropdown = React.memo(
|
|||
[
|
||||
tGridEnabled,
|
||||
alertsActionItems,
|
||||
addToCasesActionItems,
|
||||
addToCaseActionItems,
|
||||
hostIsolationActionItems,
|
||||
investigateInTimelineActionItems,
|
||||
]
|
||||
|
@ -220,26 +196,30 @@ export const TakeActionDropdown = React.memo(
|
|||
|
||||
const takeActionButton = useMemo(() => {
|
||||
return (
|
||||
<EuiButton iconSide="right" fill iconType="arrowDown" onClick={togglePopoverHandler}>
|
||||
<EuiButton
|
||||
data-test-subj="take-action-dropdown-btn"
|
||||
fill
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
onClick={togglePopoverHandler}
|
||||
>
|
||||
{TAKE_ACTION}
|
||||
</EuiButton>
|
||||
);
|
||||
}, [togglePopoverHandler]);
|
||||
|
||||
return items.length && !loadingEventDetails ? (
|
||||
<>
|
||||
<EuiPopover
|
||||
id="AlertTakeActionPanel"
|
||||
button={takeActionButton}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopoverHandler}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={items} />
|
||||
</EuiPopover>
|
||||
</>
|
||||
return items.length && !loadingEventDetails && ecsData ? (
|
||||
<EuiPopover
|
||||
id="AlertTakeActionPanel"
|
||||
button={takeActionButton}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopoverHandler}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiContextMenuPanel data-test-subj="takeActionPanelMenu" size="s" items={items} />
|
||||
</EuiPopover>
|
||||
) : null;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -60,7 +60,7 @@ export const useFetchEcsAlertsData = ({
|
|||
} catch (e) {
|
||||
if (isSubscribed) {
|
||||
if (onError) {
|
||||
onError(e);
|
||||
onError(e as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { find, get } from 'lodash/fp';
|
||||
import { find, get, isEmpty } from 'lodash/fp';
|
||||
import { TakeActionDropdown } from '../../../../detections/components/take_action_dropdown';
|
||||
import type { TimelineEventsDetailsItem } from '../../../../../common';
|
||||
import { useExceptionModal } from '../../../../detections/components/alerts_table/timeline_actions/use_add_exception_modal';
|
||||
|
@ -16,8 +16,8 @@ import { EventFiltersModal } from '../../../../management/pages/event_filters/vi
|
|||
import { useEventFilterModal } from '../../../../detections/components/alerts_table/timeline_actions/use_event_filter_modal';
|
||||
import { getFieldValue } from '../../../../detections/components/host_isolation/helpers';
|
||||
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { useFetchEcsAlertsData } from '../../../../detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import { useFetchEcsAlertsData } from '../../../../detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data';
|
||||
|
||||
interface EventDetailsFooterProps {
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
|
@ -73,7 +73,10 @@ export const EventDetailsFooter = React.memo(
|
|||
[detailsData]
|
||||
);
|
||||
|
||||
const eventIds = useMemo(() => [expandedEvent?.eventId], [expandedEvent?.eventId]);
|
||||
const eventIds = useMemo(
|
||||
() => (isEmpty(expandedEvent?.eventId) ? null : [expandedEvent?.eventId]),
|
||||
[expandedEvent?.eventId]
|
||||
);
|
||||
|
||||
const {
|
||||
exceptionModalType,
|
||||
|
@ -97,25 +100,27 @@ export const EventDetailsFooter = React.memo(
|
|||
skip: expandedEvent?.eventId == null,
|
||||
});
|
||||
|
||||
const ecsData = get(0, alertsEcsData);
|
||||
const ecsData = expandedEvent.ecsData ?? get(0, alertsEcsData);
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<TakeActionDropdown
|
||||
detailsData={detailsData}
|
||||
ecsData={ecsData}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
|
||||
loadingEventDetails={loadingEventDetails}
|
||||
onAddEventFilterClick={onAddEventFilterClick}
|
||||
onAddExceptionTypeClick={onAddExceptionTypeClick}
|
||||
onAddIsolationStatusClick={onAddIsolationStatusClick}
|
||||
refetch={expandedEvent?.refetch}
|
||||
indexName={expandedEvent.indexName}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
{ecsData && (
|
||||
<TakeActionDropdown
|
||||
detailsData={detailsData}
|
||||
ecsData={ecsData}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
|
||||
loadingEventDetails={loadingEventDetails}
|
||||
onAddEventFilterClick={onAddEventFilterClick}
|
||||
onAddExceptionTypeClick={onAddExceptionTypeClick}
|
||||
onAddIsolationStatusClick={onAddIsolationStatusClick}
|
||||
refetch={expandedEvent?.refetch}
|
||||
indexName={expandedEvent.indexName}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
createSecuritySolutionStorageMock,
|
||||
mockEcsDataWithAlert,
|
||||
} from '../../../common/mock';
|
||||
import { createStore, State } from '../../../common/store';
|
||||
import { DetailsPanel } from './index';
|
||||
|
@ -69,6 +70,7 @@ describe('Details Panel Component', () => {
|
|||
params: {
|
||||
eventId: 'my-id',
|
||||
indexName: 'my-index',
|
||||
ecsData: mockEcsDataWithAlert,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -149,7 +151,7 @@ describe('Details Panel Component', () => {
|
|||
|
||||
describe('DetailsPanel:HostDetails: rendering', () => {
|
||||
beforeEach(() => {
|
||||
state.timeline.timelineById.test.expandedDetail = hostExpandedDetail;
|
||||
state.timeline.timelineById.test.expandedDetail = hostExpandedDetail as TimelineExpandedDetail;
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
});
|
||||
|
||||
|
@ -166,7 +168,7 @@ describe('Details Panel Component', () => {
|
|||
|
||||
describe('DetailsPanel:NetworkDetails: rendering', () => {
|
||||
beforeEach(() => {
|
||||
state.timeline.timelineById.test.expandedDetail = networkExpandedDetail;
|
||||
state.timeline.timelineById.test.expandedDetail = networkExpandedDetail as TimelineExpandedDetail;
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
});
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
PinnedEvent,
|
||||
} from './pinned_event';
|
||||
import { Direction, Maybe } from '../../search_strategy';
|
||||
import { Ecs } from '../../ecs';
|
||||
|
||||
export * from './actions';
|
||||
export * from './cells';
|
||||
|
@ -475,6 +476,7 @@ export type TimelineExpandedEventType =
|
|||
params?: {
|
||||
eventId: string;
|
||||
indexName: string;
|
||||
ecsData?: Ecs;
|
||||
};
|
||||
}
|
||||
| EmptyObject;
|
||||
|
|
|
@ -127,6 +127,7 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
const updatedExpandedDetail: TimelineExpandedDetailType = {
|
||||
panelView: 'eventDetail',
|
||||
params: {
|
||||
ecsData: event.ecs,
|
||||
eventId,
|
||||
indexName,
|
||||
},
|
||||
|
@ -139,7 +140,7 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
timelineId,
|
||||
})
|
||||
);
|
||||
}, [dispatch, event._id, event._index, tabType, timelineId]);
|
||||
}, [dispatch, event._id, event._index, event.ecs, tabType, timelineId]);
|
||||
|
||||
const setEventsLoading = useCallback<SetEventsLoading>(
|
||||
({ eventIds, isLoading }) => {
|
||||
|
|
|
@ -85,6 +85,7 @@ const RowActionComponent = ({
|
|||
params: {
|
||||
eventId,
|
||||
indexName: indexName ?? '',
|
||||
ecsData,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -95,7 +96,7 @@ const RowActionComponent = ({
|
|||
timelineId,
|
||||
})
|
||||
);
|
||||
}, [dispatch, eventId, indexName, tabType, timelineId]);
|
||||
}, [dispatch, ecsData, eventId, indexName, tabType, timelineId]);
|
||||
|
||||
const Action = controlColumn.rowCellRender;
|
||||
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { normalizedEventFields } from './use_add_to_case';
|
||||
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
|
||||
import { merge } from 'lodash';
|
||||
|
||||
const defaultArgs = {
|
||||
_id: 'test-id',
|
||||
data: [
|
||||
{ field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] },
|
||||
{ field: ALERT_RULE_UUID, value: ['data-rule-id'] },
|
||||
{ field: ALERT_RULE_NAME, value: ['data-rule-name'] },
|
||||
],
|
||||
ecs: {
|
||||
_id: 'test-id',
|
||||
_index: 'test-index',
|
||||
signal: { rule: { id: ['rule-id'], name: ['rule-name'], false_positives: [] } },
|
||||
},
|
||||
};
|
||||
describe('normalizedEventFields', () => {
|
||||
it('uses rule data when provided', () => {
|
||||
const result = normalizedEventFields(defaultArgs);
|
||||
expect(result).toEqual({
|
||||
ruleId: 'data-rule-id',
|
||||
ruleName: 'data-rule-name',
|
||||
});
|
||||
});
|
||||
const makeObj = (s: string, v: string[]) => {
|
||||
const keys = s.split('.');
|
||||
return keys
|
||||
.reverse()
|
||||
.reduce((prev, current, i) => (i === 0 ? { [current]: v } : { [current]: { ...prev } }), {});
|
||||
};
|
||||
it('uses rule/ecs combo Xavier thinks is a thing but Steph has yet to see', () => {
|
||||
const args = {
|
||||
...defaultArgs,
|
||||
data: [],
|
||||
ecs: {
|
||||
_id: 'string',
|
||||
...merge(
|
||||
makeObj(ALERT_RULE_UUID, ['xavier-rule-id']),
|
||||
makeObj(ALERT_RULE_NAME, ['xavier-rule-name'])
|
||||
),
|
||||
},
|
||||
};
|
||||
const result = normalizedEventFields(args);
|
||||
expect(result).toEqual({
|
||||
ruleId: 'xavier-rule-id',
|
||||
ruleName: 'xavier-rule-name',
|
||||
});
|
||||
});
|
||||
it('falls back to use ecs data', () => {
|
||||
const result = normalizedEventFields({ ...defaultArgs, data: [] });
|
||||
expect(result).toEqual({
|
||||
ruleId: 'rule-id',
|
||||
ruleName: 'rule-name',
|
||||
});
|
||||
});
|
||||
it('returns null when all the data is bad', () => {
|
||||
const result = normalizedEventFields({ ...defaultArgs, data: [], ecs: { _id: 'bad' } });
|
||||
expect(result).toEqual({
|
||||
ruleId: null,
|
||||
ruleName: null,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { isEmpty } from 'lodash';
|
||||
import { get, isEmpty } from 'lodash/fp';
|
||||
import { useState, useCallback, useMemo, SyntheticEvent } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
@ -16,7 +16,7 @@ import { TimelineItem } from '../../common/';
|
|||
import { tGridActions } from '../store/t_grid';
|
||||
import { useDeepEqualSelector } from './use_selector';
|
||||
import { createUpdateSuccessToaster } from '../components/actions/timeline/cases/helpers';
|
||||
import { AddToCaseActionProps } from '../components/actions/timeline/cases/add_to_case_action';
|
||||
import { AddToCaseActionProps } from '../components/actions';
|
||||
|
||||
interface UseAddToCase {
|
||||
addNewCaseClick: () => void;
|
||||
|
@ -243,12 +243,24 @@ export const useAddToCase = ({
|
|||
};
|
||||
|
||||
export function normalizedEventFields(event?: TimelineItem) {
|
||||
const ruleUuid = event && event.data.find(({ field }) => field === ALERT_RULE_UUID);
|
||||
const ruleName = event && event.data.find(({ field }) => field === ALERT_RULE_NAME);
|
||||
const ruleUuidValue = ruleUuid && ruleUuid.value && ruleUuid.value[0];
|
||||
const ruleNameValue = ruleName && ruleName.value && ruleName.value[0];
|
||||
const ruleUuidData = event && event.data.find(({ field }) => field === ALERT_RULE_UUID);
|
||||
const ruleNameData = event && event.data.find(({ field }) => field === ALERT_RULE_NAME);
|
||||
const ruleUuidValueData = ruleUuidData && ruleUuidData.value && ruleUuidData.value[0];
|
||||
const ruleNameValueData = ruleNameData && ruleNameData.value && ruleNameData.value[0];
|
||||
|
||||
const ruleUuid =
|
||||
ruleUuidValueData ??
|
||||
get(`ecs.${ALERT_RULE_UUID}[0]`, event) ??
|
||||
get(`ecs.signal.rule.id[0]`, event) ??
|
||||
null;
|
||||
const ruleName =
|
||||
ruleNameValueData ??
|
||||
get(`ecs.${ALERT_RULE_NAME}[0]`, event) ??
|
||||
get(`ecs.signal.rule.name[0]`, event) ??
|
||||
null;
|
||||
|
||||
return {
|
||||
ruleId: ruleUuidValue ?? null,
|
||||
ruleName: ruleNameValue ?? null,
|
||||
ruleId: ruleUuid,
|
||||
ruleName,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -28,4 +28,8 @@ export const createTGridMocks = () => ({
|
|||
getUseAddToTimeline: () => useAddToTimeline,
|
||||
getUseAddToTimelineSensor: () => useAddToTimelineSensor,
|
||||
getUseDraggableKeyboardWrapper: () => useDraggableKeyboardWrapper,
|
||||
// eslint-disable-next-line react/display-name
|
||||
getAddToExistingCaseButton: () => <div data-test-subj="add-to-existing-case" />,
|
||||
// eslint-disable-next-line react/display-name
|
||||
getAddToNewCaseButton: () => <div data-test-subj="add-to-new-case" />,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue