mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution] Remove a data fetching hook from the add to timeline action component (#124331)
* Fetch alert ecs data in actions.tsx and not a hook in every table row * Add error handling and tests for theshold timelines * Fix bad merge * Remove unused imports * Actually remove unused file * Remove usage of alertIds and dead code from cases * Add basic sanity tests that ensure no extra network calls are being made * Remove unused operator * Remove unused imports * Remove unused mock
This commit is contained in:
parent
c299aabcb3
commit
e312c36e4c
18 changed files with 579 additions and 253 deletions
|
@ -24,8 +24,6 @@ export const timelineIntegrationMock = {
|
|||
useInsertTimeline: jest.fn(),
|
||||
},
|
||||
ui: {
|
||||
renderInvestigateInTimelineActionComponent: () =>
|
||||
mockTimelineComponent('investigate-in-timeline'),
|
||||
renderTimelineDetailsPanel: () => mockTimelineComponent('timeline-details-panel'),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -356,9 +356,6 @@ export const CaseViewPage = React.memo<CaseViewPageProps>(
|
|||
isLoadingUserActions={isLoadingUserActions}
|
||||
onShowAlertDetails={onShowAlertDetails}
|
||||
onUpdateField={onUpdateField}
|
||||
renderInvestigateInTimelineActionComponent={
|
||||
timelineUi?.renderInvestigateInTimelineActionComponent
|
||||
}
|
||||
statusActionButton={
|
||||
userCanCrud ? (
|
||||
<StatusActionButton
|
||||
|
|
|
@ -42,7 +42,6 @@ export interface CasesTimelineIntegration {
|
|||
) => UseInsertTimelineReturn;
|
||||
};
|
||||
ui?: {
|
||||
renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element;
|
||||
renderTimelineDetailsPanel?: () => JSX.Element;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ export interface UserActionTreeProps {
|
|||
onRuleDetailsClick?: RuleDetailsNavigation['onClick'];
|
||||
onShowAlertDetails: (alertId: string, index: string) => void;
|
||||
onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void;
|
||||
renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element;
|
||||
statusActionButton: JSX.Element | null;
|
||||
updateCase: (newCase: Case) => void;
|
||||
useFetchAlertData: (alertIds: string[]) => [boolean, Record<string, Ecs>];
|
||||
|
|
|
@ -12,7 +12,6 @@ import { TimelineId } from '../../../common/types/timeline';
|
|||
|
||||
import { getRuleDetailsUrl, useFormatUrl } from '../../common/components/link_to';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { useGetUserCasesPermissions, useKibana, useNavigation } from '../../common/lib/kibana';
|
||||
import { APP_ID, CASES_PATH, SecurityPageName } from '../../../common/constants';
|
||||
import { timelineActions } from '../../timelines/store/timeline';
|
||||
|
@ -25,7 +24,6 @@ import { SpyRoute } from '../../common/utils/route/spy_routes';
|
|||
import { useInsertTimeline } from '../components/use_insert_timeline';
|
||||
import * as timelineMarkdownPlugin from '../../common/components/markdown_editor/plugins/timeline';
|
||||
import { DetailsPanel } from '../../timelines/components/side_panel';
|
||||
import { InvestigateInTimelineAction } from '../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action';
|
||||
import { useFetchAlertData } from './use_fetch_alert_data';
|
||||
|
||||
const TimelineDetailsPanel = () => {
|
||||
|
@ -44,17 +42,6 @@ const TimelineDetailsPanel = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const InvestigateInTimelineActionComponent = (alertIds: string[]) => {
|
||||
return (
|
||||
<InvestigateInTimelineAction
|
||||
ariaLabel={i18n.SEND_ALERT_TO_TIMELINE}
|
||||
alertIds={alertIds}
|
||||
key="investigate-in-timeline"
|
||||
ecsRowData={null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CaseContainerComponent: React.FC = () => {
|
||||
const { cases: casesUi } = useKibana().services;
|
||||
const { getAppUrl, navigateTo } = useNavigation();
|
||||
|
@ -163,7 +150,6 @@ const CaseContainerComponent: React.FC = () => {
|
|||
useInsertTimeline,
|
||||
},
|
||||
ui: {
|
||||
renderInvestigateInTimelineActionComponent: InvestigateInTimelineActionComponent,
|
||||
renderTimelineDetailsPanel: TimelineDetailsPanel,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
import { CoreStart } from '../../../../../../../src/core/public';
|
||||
import { StartPlugins } from '../../../types';
|
||||
|
||||
type GlobalServices = Pick<CoreStart, 'http' | 'uiSettings'> & Pick<StartPlugins, 'data'>;
|
||||
type GlobalServices = Pick<CoreStart, 'http' | 'uiSettings' | 'notifications'> &
|
||||
Pick<StartPlugins, 'data'>;
|
||||
|
||||
export class KibanaServices {
|
||||
private static kibanaVersion?: string;
|
||||
|
@ -19,8 +20,9 @@ export class KibanaServices {
|
|||
data,
|
||||
kibanaVersion,
|
||||
uiSettings,
|
||||
notifications,
|
||||
}: GlobalServices & { kibanaVersion: string }) {
|
||||
this.services = { data, http, uiSettings };
|
||||
this.services = { data, http, uiSettings, notifications };
|
||||
this.kibanaVersion = kibanaVersion;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,11 +29,18 @@ import type { ISearchStart } from '../../../../../../../src/plugins/data/public'
|
|||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
import { getTimelineTemplate } from '../../../timelines/containers/api';
|
||||
import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
|
||||
import { KibanaServices } from '../../../common/lib/kibana';
|
||||
import {
|
||||
DEFAULT_FROM_MOMENT,
|
||||
DEFAULT_TO_MOMENT,
|
||||
} from '../../../common/utils/default_date_settings';
|
||||
|
||||
jest.mock('../../../timelines/containers/api', () => ({
|
||||
getTimelineTemplate: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
describe('alert actions', () => {
|
||||
const anchor = '2020-03-01T17:59:46.349Z';
|
||||
const unix = moment(anchor).valueOf();
|
||||
|
@ -41,6 +48,9 @@ describe('alert actions', () => {
|
|||
let updateTimelineIsLoading: UpdateTimelineLoading;
|
||||
let searchStrategyClient: jest.Mocked<ISearchStart>;
|
||||
let clock: sinon.SinonFakeTimers;
|
||||
let mockKibanaServices: jest.Mock;
|
||||
let fetchMock: jest.Mock;
|
||||
let toastMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
// jest carries state between mocked implementations when using
|
||||
|
@ -52,6 +62,14 @@ describe('alert actions', () => {
|
|||
|
||||
createTimeline = jest.fn() as jest.Mocked<CreateTimeline>;
|
||||
updateTimelineIsLoading = jest.fn() as jest.Mocked<UpdateTimelineLoading>;
|
||||
mockKibanaServices = KibanaServices.get as jest.Mock;
|
||||
|
||||
fetchMock = jest.fn();
|
||||
toastMock = jest.fn();
|
||||
mockKibanaServices.mockReturnValue({
|
||||
http: { fetch: fetchMock },
|
||||
notifications: { toasts: { addError: toastMock } },
|
||||
});
|
||||
|
||||
searchStrategyClient = {
|
||||
...dataPluginMock.createStartContract().search,
|
||||
|
@ -418,6 +436,59 @@ describe('alert actions', () => {
|
|||
});
|
||||
|
||||
describe('determineToAndFrom', () => {
|
||||
const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({
|
||||
...mockAADEcsDataWithAlert,
|
||||
kibana: {
|
||||
alert: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert,
|
||||
rule: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule,
|
||||
parameters: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters,
|
||||
threshold: {
|
||||
field: ['destination.ip'],
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
name: ['mock threshold rule'],
|
||||
saved_id: [],
|
||||
type: ['threshold'],
|
||||
uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'],
|
||||
timeline_id: undefined,
|
||||
timeline_title: undefined,
|
||||
},
|
||||
threshold_result: {
|
||||
count: 99,
|
||||
from: '2021-01-10T21:11:45.839Z',
|
||||
cardinality: [
|
||||
{
|
||||
field: 'source.ip',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
terms: [
|
||||
{
|
||||
field: 'destination.ip',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
beforeEach(() => {
|
||||
fetchMock.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_id: ecsDataMockWithNoTemplateTimeline[0]._id,
|
||||
_index: 'mock',
|
||||
_source: ecsDataMockWithNoTemplateTimeline[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
test('it uses ecs.Data.timestamp if one is provided', () => {
|
||||
const ecsDataMock: Ecs = {
|
||||
...mockEcsDataWithAlert,
|
||||
|
@ -438,47 +509,6 @@ describe('alert actions', () => {
|
|||
});
|
||||
|
||||
test('it uses original_time and threshold_result.from for threshold alerts', async () => {
|
||||
const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({
|
||||
...mockAADEcsDataWithAlert,
|
||||
kibana: {
|
||||
alert: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert,
|
||||
rule: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule,
|
||||
parameters: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters,
|
||||
threshold: {
|
||||
field: ['destination.ip'],
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
name: ['mock threshold rule'],
|
||||
saved_id: [],
|
||||
type: ['threshold'],
|
||||
uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'],
|
||||
timeline_id: undefined,
|
||||
timeline_title: undefined,
|
||||
},
|
||||
threshold_result: {
|
||||
count: 99,
|
||||
from: '2021-01-10T21:11:45.839Z',
|
||||
cardinality: [
|
||||
{
|
||||
field: 'source.ip',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
terms: [
|
||||
{
|
||||
field: 'destination.ip',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const expectedFrom = '2021-01-10T21:11:45.839Z';
|
||||
const expectedTo = '2021-01-10T21:12:45.839Z';
|
||||
|
||||
|
@ -525,4 +555,86 @@ describe('alert actions', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('show toasts when data is malformed', () => {
|
||||
const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({
|
||||
...mockAADEcsDataWithAlert,
|
||||
kibana: {
|
||||
alert: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert,
|
||||
rule: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule,
|
||||
parameters: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters,
|
||||
threshold: {
|
||||
field: ['destination.ip'],
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
name: ['mock threshold rule'],
|
||||
saved_id: [],
|
||||
type: ['threshold'],
|
||||
uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'],
|
||||
timeline_id: undefined,
|
||||
timeline_title: undefined,
|
||||
},
|
||||
threshold_result: {
|
||||
count: 99,
|
||||
from: '2021-01-10T21:11:45.839Z',
|
||||
cardinality: [
|
||||
{
|
||||
field: 'source.ip',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
terms: [
|
||||
{
|
||||
field: 'destination.ip',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
beforeEach(() => {
|
||||
fetchMock.mockResolvedValue({
|
||||
hits: 'not correctly formed doc',
|
||||
});
|
||||
});
|
||||
test('renders a toast and calls create timeline with basic defaults', async () => {
|
||||
const expectedFrom = DEFAULT_FROM_MOMENT.toISOString();
|
||||
const expectedTo = DEFAULT_TO_MOMENT.toISOString();
|
||||
const timelineProps = {
|
||||
...defaultTimelineProps,
|
||||
timeline: {
|
||||
...defaultTimelineProps.timeline,
|
||||
dataProviders: [],
|
||||
dateRange: {
|
||||
start: expectedFrom,
|
||||
end: expectedTo,
|
||||
},
|
||||
description: '',
|
||||
kqlQuery: {
|
||||
filterQuery: null,
|
||||
},
|
||||
resolveTimelineConfig: undefined,
|
||||
},
|
||||
from: expectedFrom,
|
||||
to: expectedTo,
|
||||
};
|
||||
|
||||
delete timelineProps.ruleNote;
|
||||
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: ecsDataMockWithNoTemplateTimeline,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
});
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith(timelineProps);
|
||||
expect(toastMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
ALERT_RULE_PARAMETERS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
ALERT_ORIGINAL_TIME,
|
||||
ALERT_GROUP_ID,
|
||||
|
@ -64,6 +65,13 @@ import {
|
|||
QueryOperator,
|
||||
} from '../../../timelines/components/timeline/data_providers/data_provider';
|
||||
import { getTimelineTemplate } from '../../../timelines/containers/api';
|
||||
import { KibanaServices } from '../../../common/lib/kibana';
|
||||
import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../common/constants';
|
||||
import { buildAlertsQuery, formatAlertToEcsSignal } from '../../../common/utils/alerts';
|
||||
import {
|
||||
DEFAULT_FROM_MOMENT,
|
||||
DEFAULT_TO_MOMENT,
|
||||
} from '../../../common/utils/default_date_settings';
|
||||
|
||||
export const getUpdateAlertsQuery = (eventIds: Readonly<string[]>) => {
|
||||
return {
|
||||
|
@ -177,7 +185,7 @@ export const getThresholdAggregationData = (ecsData: Ecs | Ecs[]): ThresholdAggr
|
|||
return thresholdEcsData.reduce<ThresholdAggregationData>(
|
||||
(outerAcc, thresholdData) => {
|
||||
const threshold =
|
||||
getField(thresholdData, ALERT_RULE_PARAMETERS).threshold ??
|
||||
getField(thresholdData, `${ALERT_RULE_PARAMETERS}.threshold`) ??
|
||||
thresholdData.signal?.rule?.threshold;
|
||||
|
||||
const thresholdResult: {
|
||||
|
@ -384,47 +392,102 @@ const buildEqlDataProviderOrFilter = (
|
|||
return { filters: [], dataProviders: [] };
|
||||
};
|
||||
|
||||
const createThresholdTimeline = (
|
||||
const createThresholdTimeline = async (
|
||||
ecsData: Ecs,
|
||||
createTimeline: ({ from, timeline, to }: CreateTimelineProps) => void,
|
||||
noteContent: string,
|
||||
templateValues: { filters?: Filter[]; query?: string; dataProviders?: DataProvider[] }
|
||||
) => {
|
||||
const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData(ecsData);
|
||||
const params = getField(ecsData, ALERT_RULE_PARAMETERS);
|
||||
const filters = getFiltersFromRule(params.filters ?? ecsData.signal?.rule?.filters) ?? [];
|
||||
const language = params.language ?? ecsData.signal?.rule?.language ?? 'kuery';
|
||||
const query = params.query ?? ecsData.signal?.rule?.query ?? '';
|
||||
const indexNames = params.index ?? ecsData.signal?.rule?.index ?? [];
|
||||
|
||||
return createTimeline({
|
||||
from: thresholdFrom,
|
||||
notes: null,
|
||||
timeline: {
|
||||
...timelineDefaults,
|
||||
description: `_id: ${ecsData._id}`,
|
||||
filters: templateValues.filters ?? filters,
|
||||
dataProviders: templateValues.dataProviders ?? dataProviders,
|
||||
id: TimelineId.active,
|
||||
indexNames,
|
||||
dateRange: {
|
||||
start: thresholdFrom,
|
||||
end: thresholdTo,
|
||||
},
|
||||
eventType: 'all',
|
||||
kqlQuery: {
|
||||
filterQuery: {
|
||||
kuery: {
|
||||
kind: language,
|
||||
expression: templateValues.query ?? query,
|
||||
try {
|
||||
const alertResponse = await KibanaServices.get().http.fetch<
|
||||
estypes.SearchResponse<{ '@timestamp': string; [key: string]: unknown }>
|
||||
>(DETECTION_ENGINE_QUERY_SIGNALS_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(buildAlertsQuery([ecsData._id])),
|
||||
});
|
||||
const formattedAlertData =
|
||||
alertResponse?.hits.hits.reduce<Ecs[]>((acc, { _id, _index, _source = {} }) => {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
...formatAlertToEcsSignal(_source),
|
||||
_id,
|
||||
_index,
|
||||
timestamp: _source['@timestamp'],
|
||||
},
|
||||
];
|
||||
}, []) ?? [];
|
||||
const alertDoc = formattedAlertData[0];
|
||||
const params = getField(alertDoc, ALERT_RULE_PARAMETERS);
|
||||
const filters = getFiltersFromRule(params.filters ?? alertDoc.signal?.rule?.filters) ?? [];
|
||||
const language = params.language ?? alertDoc.signal?.rule?.language ?? 'kuery';
|
||||
const query = params.query ?? alertDoc.signal?.rule?.query ?? '';
|
||||
const indexNames = params.index ?? alertDoc.signal?.rule?.index ?? [];
|
||||
|
||||
const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData(alertDoc);
|
||||
return createTimeline({
|
||||
from: thresholdFrom,
|
||||
notes: null,
|
||||
timeline: {
|
||||
...timelineDefaults,
|
||||
description: `_id: ${alertDoc._id}`,
|
||||
filters: templateValues.filters ?? filters,
|
||||
dataProviders: templateValues.dataProviders ?? dataProviders,
|
||||
id: TimelineId.active,
|
||||
indexNames,
|
||||
dateRange: {
|
||||
start: thresholdFrom,
|
||||
end: thresholdTo,
|
||||
},
|
||||
eventType: 'all',
|
||||
kqlQuery: {
|
||||
filterQuery: {
|
||||
kuery: {
|
||||
kind: language,
|
||||
expression: templateValues.query ?? query,
|
||||
},
|
||||
serializedQuery: templateValues.query ?? query,
|
||||
},
|
||||
serializedQuery: templateValues.query ?? query,
|
||||
},
|
||||
},
|
||||
},
|
||||
to: thresholdTo,
|
||||
ruleNote: noteContent,
|
||||
});
|
||||
to: thresholdTo,
|
||||
ruleNote: noteContent,
|
||||
});
|
||||
} catch (error) {
|
||||
const { toasts } = KibanaServices.get().notifications;
|
||||
toasts.addError(error, {
|
||||
toastMessage: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.alerts.createThresholdTimelineFailure',
|
||||
{
|
||||
defaultMessage: 'Failed to create timeline for document _id: {id}',
|
||||
values: { id: ecsData._id },
|
||||
}
|
||||
),
|
||||
title: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.alerts.createThresholdTimelineFailureTitle',
|
||||
{
|
||||
defaultMessage: 'Failed to create theshold alert timeline',
|
||||
}
|
||||
),
|
||||
});
|
||||
const from = DEFAULT_FROM_MOMENT.toISOString();
|
||||
const to = DEFAULT_TO_MOMENT.toISOString();
|
||||
return createTimeline({
|
||||
from,
|
||||
notes: null,
|
||||
timeline: {
|
||||
...timelineDefaults,
|
||||
id: TimelineId.active,
|
||||
indexNames: [],
|
||||
dateRange: {
|
||||
start: from,
|
||||
end: to,
|
||||
},
|
||||
eventType: 'all',
|
||||
},
|
||||
to,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const sendAlertToTimelineAction = async ({
|
||||
|
@ -492,7 +555,7 @@ export const sendAlertToTimelineAction = async ({
|
|||
);
|
||||
// threshold with template
|
||||
if (isThresholdRule(ecsData)) {
|
||||
createThresholdTimeline(ecsData, createTimeline, noteContent, {
|
||||
return createThresholdTimeline(ecsData, createTimeline, noteContent, {
|
||||
filters,
|
||||
query,
|
||||
dataProviders,
|
||||
|
@ -550,7 +613,7 @@ export const sendAlertToTimelineAction = async ({
|
|||
});
|
||||
}
|
||||
} else if (isThresholdRule(ecsData)) {
|
||||
createThresholdTimeline(ecsData, createTimeline, noteContent, {});
|
||||
return createThresholdTimeline(ecsData, createTimeline, noteContent, {});
|
||||
} else {
|
||||
let { dataProviders, filters } = buildTimelineDataProviderOrFilter(alertIds ?? [], ecsData._id);
|
||||
if (isEqlRuleWithGroupId(ecsData)) {
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { fireEvent, render, act } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { KibanaServices, useKibana } from '../../../../common/lib/kibana';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import * as actions from '../actions';
|
||||
import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
||||
import type { SendAlertToTimelineActionProps } from '../types';
|
||||
import { InvestigateInTimelineAction } from './investigate_in_timeline_action';
|
||||
|
||||
const ecsRowData: Ecs = {
|
||||
_id: '1',
|
||||
agent: { type: ['blah'] },
|
||||
kibana: {
|
||||
alert: {
|
||||
workflow_status: ['open'],
|
||||
rule: {
|
||||
parameters: {},
|
||||
uuid: ['testId'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../actions');
|
||||
|
||||
const props = {
|
||||
ecsRowData,
|
||||
onInvestigateInTimelineAlertClick: () => {},
|
||||
ariaLabel: 'test',
|
||||
};
|
||||
|
||||
describe('use investigate in timeline hook', () => {
|
||||
let mockSendAlertToTimeline: jest.SpyInstance<Promise<void>, [SendAlertToTimelineActionProps]>;
|
||||
|
||||
beforeEach(() => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
(KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock);
|
||||
mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction');
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
search: {
|
||||
searchStrategyClient: jest.fn(),
|
||||
},
|
||||
query: jest.fn(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
test('it creates a component and click handler', () => {
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<InvestigateInTimelineAction {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.getByTestId('send-alert-to-timeline-button')).toBeTruthy();
|
||||
});
|
||||
test('it calls sendAlertToTimelineAction once on click, not on mount', () => {
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<InvestigateInTimelineAction {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(0);
|
||||
act(() => {
|
||||
fireEvent.click(wrapper.getByTestId('send-alert-to-timeline-button'));
|
||||
});
|
||||
expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -19,21 +19,18 @@ import { useInvestigateInTimeline } from './use_investigate_in_timeline';
|
|||
interface InvestigateInTimelineActionProps {
|
||||
ecsRowData?: Ecs | Ecs[] | null;
|
||||
ariaLabel?: string;
|
||||
alertIds?: string[];
|
||||
buttonType?: 'text' | 'icon';
|
||||
onInvestigateInTimelineAlertClick?: () => void;
|
||||
}
|
||||
|
||||
const InvestigateInTimelineActionComponent: React.FC<InvestigateInTimelineActionProps> = ({
|
||||
ariaLabel = ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL,
|
||||
alertIds,
|
||||
ecsRowData,
|
||||
buttonType,
|
||||
onInvestigateInTimelineAlertClick,
|
||||
}) => {
|
||||
const { investigateInTimelineAlertClick } = useInvestigateInTimeline({
|
||||
ecsRowData,
|
||||
alertIds,
|
||||
onInvestigateInTimelineAlertClick,
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { KibanaServices, useKibana } from '../../../../common/lib/kibana';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import { useInvestigateInTimeline } from './use_investigate_in_timeline';
|
||||
import * as actions from '../actions';
|
||||
import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
||||
import type { SendAlertToTimelineActionProps } from '../types';
|
||||
|
||||
const ecsRowData: Ecs = {
|
||||
_id: '1',
|
||||
agent: { type: ['blah'] },
|
||||
kibana: {
|
||||
alert: {
|
||||
workflow_status: ['open'],
|
||||
rule: {
|
||||
parameters: {},
|
||||
uuid: ['testId'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../actions');
|
||||
|
||||
const props = {
|
||||
ecsRowData,
|
||||
onInvestigateInTimelineAlertClick: () => {},
|
||||
};
|
||||
|
||||
describe('use investigate in timeline hook', () => {
|
||||
let mockSendAlertToTimeline: jest.SpyInstance<Promise<void>, [SendAlertToTimelineActionProps]>;
|
||||
|
||||
beforeEach(() => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
(KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock);
|
||||
mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction');
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
search: {
|
||||
searchStrategyClient: jest.fn(),
|
||||
},
|
||||
query: jest.fn(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
test('it creates a component and click handler', () => {
|
||||
const { result } = renderHook(() => useInvestigateInTimeline(props), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(result.current.investigateInTimelineActionItems).toBeTruthy();
|
||||
expect(typeof result.current.investigateInTimelineAlertClick).toBe('function');
|
||||
});
|
||||
|
||||
describe('the click handler calls createTimeline once and only once', () => {
|
||||
test('runs 0 times on render, once on click', async () => {
|
||||
const { result } = renderHook(() => useInvestigateInTimeline(props), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
const component = result.current.investigateInTimelineActionItems[0];
|
||||
const { getByTestId } = render(component);
|
||||
expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(0);
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId('investigate-in-timeline-action-item'));
|
||||
});
|
||||
expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,32 +6,27 @@
|
|||
*/
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { TimelineId, TimelineType } from '../../../../../common/types/timeline';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline';
|
||||
import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline';
|
||||
import { sendAlertToTimelineAction } from '../actions';
|
||||
import { dispatchUpdateTimeline } from '../../../../timelines/components/open_timeline/helpers';
|
||||
import { useCreateTimeline } from '../../../../timelines/components/timeline/properties/use_create_timeline';
|
||||
import { CreateTimelineProps } from '../types';
|
||||
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../translations';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { useFetchEcsAlertsData } from '../../../containers/detection_engine/alerts/use_fetch_ecs_alerts_data';
|
||||
|
||||
interface UseInvestigateInTimelineActionProps {
|
||||
ecsRowData?: Ecs | Ecs[] | null;
|
||||
nonEcsRowData?: TimelineNonEcsData[];
|
||||
alertIds?: string[] | null | undefined;
|
||||
onInvestigateInTimelineAlertClick?: () => void;
|
||||
}
|
||||
|
||||
export const useInvestigateInTimeline = ({
|
||||
ecsRowData,
|
||||
alertIds,
|
||||
onInvestigateInTimelineAlertClick,
|
||||
}: UseInvestigateInTimelineActionProps) => {
|
||||
const {
|
||||
|
@ -54,8 +49,14 @@ export const useInvestigateInTimeline = ({
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
const clearActiveTimeline = useCreateTimeline({
|
||||
timelineId: TimelineId.active,
|
||||
timelineType: TimelineType.default,
|
||||
});
|
||||
|
||||
const createTimeline = useCallback(
|
||||
({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => {
|
||||
clearActiveTimeline();
|
||||
updateTimelineIsLoading({ id: TimelineId.active, isLoading: false });
|
||||
dispatchUpdateTimeline(dispatch)({
|
||||
duplicate: true,
|
||||
|
@ -72,27 +73,14 @@ export const useInvestigateInTimeline = ({
|
|||
ruleNote,
|
||||
})();
|
||||
},
|
||||
[dispatch, filterManager, updateTimelineIsLoading]
|
||||
[dispatch, filterManager, updateTimelineIsLoading, clearActiveTimeline]
|
||||
);
|
||||
|
||||
const showInvestigateInTimelineAction = alertIds != null;
|
||||
const { isLoading: isFetchingAlertEcs, alertsEcsData } = useFetchEcsAlertsData({
|
||||
alertIds,
|
||||
skip: alertIds == null,
|
||||
});
|
||||
|
||||
const investigateInTimelineAlertClick = useCallback(async () => {
|
||||
if (onInvestigateInTimelineAlertClick) {
|
||||
onInvestigateInTimelineAlertClick();
|
||||
}
|
||||
if (!isEmpty(alertsEcsData) && alertsEcsData !== null) {
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: alertsEcsData,
|
||||
searchStrategyClient,
|
||||
updateTimelineIsLoading,
|
||||
});
|
||||
} else if (ecsRowData != null) {
|
||||
if (ecsRowData != null) {
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: ecsRowData,
|
||||
|
@ -101,7 +89,6 @@ export const useInvestigateInTimeline = ({
|
|||
});
|
||||
}
|
||||
}, [
|
||||
alertsEcsData,
|
||||
createTimeline,
|
||||
ecsRowData,
|
||||
onInvestigateInTimelineAlertClick,
|
||||
|
@ -109,22 +96,22 @@ export const useInvestigateInTimeline = ({
|
|||
updateTimelineIsLoading,
|
||||
]);
|
||||
|
||||
const investigateInTimelineActionItems = showInvestigateInTimelineAction
|
||||
? [
|
||||
<EuiContextMenuItem
|
||||
key="investigate-in-timeline-action-item"
|
||||
data-test-subj="investigate-in-timeline-action-item"
|
||||
disabled={ecsRowData == null && isFetchingAlertEcs === true}
|
||||
onClick={investigateInTimelineAlertClick}
|
||||
>
|
||||
{ACTION_INVESTIGATE_IN_TIMELINE}
|
||||
</EuiContextMenuItem>,
|
||||
]
|
||||
: [];
|
||||
const investigateInTimelineActionItems = useMemo(
|
||||
() => [
|
||||
<EuiContextMenuItem
|
||||
key="investigate-in-timeline-action-item"
|
||||
data-test-subj="investigate-in-timeline-action-item"
|
||||
disabled={ecsRowData == null}
|
||||
onClick={investigateInTimelineAlertClick}
|
||||
>
|
||||
{ACTION_INVESTIGATE_IN_TIMELINE}
|
||||
</EuiContextMenuItem>,
|
||||
],
|
||||
[ecsRowData, investigateInTimelineAlertClick]
|
||||
);
|
||||
|
||||
return {
|
||||
investigateInTimelineActionItems,
|
||||
investigateInTimelineAlertClick,
|
||||
showInvestigateInTimelineAction,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
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/search_strategy';
|
||||
import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations';
|
||||
import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions';
|
||||
|
@ -81,10 +80,6 @@ export const TakeActionDropdown = React.memo(
|
|||
[detailsData]
|
||||
);
|
||||
|
||||
const alertIds = useMemo(
|
||||
() => (isEmpty(actionsData.eventId) ? null : [actionsData.eventId]),
|
||||
[actionsData.eventId]
|
||||
);
|
||||
const isEvent = actionsData.eventKind === 'event';
|
||||
|
||||
const isAgentEndpoint = useMemo(() => ecsData?.agent?.type?.includes('endpoint'), [ecsData]);
|
||||
|
@ -156,7 +151,6 @@ export const TakeActionDropdown = React.memo(
|
|||
});
|
||||
|
||||
const { investigateInTimelineActionItems } = useInvestigateInTimeline({
|
||||
alertIds,
|
||||
ecsRowData: ecsData,
|
||||
onInvestigateInTimelineAlertClick: closePopoverHandler,
|
||||
});
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
|
||||
import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants';
|
||||
import { KibanaServices } from '../../../../common/lib/kibana';
|
||||
import { buildAlertsQuery, formatAlertToEcsSignal } from '../../../../common/utils/alerts';
|
||||
|
||||
export const useFetchEcsAlertsData = ({
|
||||
alertIds,
|
||||
skip,
|
||||
onError,
|
||||
}: {
|
||||
alertIds?: string[] | null | undefined;
|
||||
skip?: boolean;
|
||||
onError?: (e: Error) => void;
|
||||
}): { isLoading: boolean | null; alertsEcsData: Ecs[] | null } => {
|
||||
const [isLoading, setIsLoading] = useState<boolean | null>(null);
|
||||
const [alertsEcsData, setAlertEcsData] = useState<Ecs[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const fetchAlert = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const alertResponse = await KibanaServices.get().http.fetch<
|
||||
estypes.SearchResponse<{ '@timestamp': string; [key: string]: unknown }>
|
||||
>(DETECTION_ENGINE_QUERY_SIGNALS_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(buildAlertsQuery(alertIds ?? [])),
|
||||
});
|
||||
|
||||
setAlertEcsData(
|
||||
alertResponse?.hits.hits.reduce<Ecs[]>(
|
||||
(acc, { _id, _index, _source = {} }) => [
|
||||
...acc,
|
||||
{
|
||||
...formatAlertToEcsSignal(_source),
|
||||
_id,
|
||||
_index,
|
||||
timestamp: _source['@timestamp'],
|
||||
},
|
||||
],
|
||||
[]
|
||||
) ?? []
|
||||
);
|
||||
} catch (e) {
|
||||
if (isSubscribed) {
|
||||
if (onError) {
|
||||
onError(e as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isSubscribed) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isEmpty(alertIds) && !skip) {
|
||||
fetchAlert();
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [alertIds, onError, skip]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
alertsEcsData,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { EventDetailsFooter } from './footer';
|
||||
import '../../../../common/mock/match_media';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import { mockAlertDetailsData } from '../../../../common/components/event_details/__mocks__';
|
||||
import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy';
|
||||
import { KibanaServices, useKibana } from '../../../../common/lib/kibana';
|
||||
import { coreMock } from '../../../../../../../../src/core/public/mocks';
|
||||
|
||||
const ecsData: Ecs = {
|
||||
_id: '1',
|
||||
agent: { type: ['blah'] },
|
||||
kibana: {
|
||||
alert: {
|
||||
workflow_status: ['open'],
|
||||
rule: {
|
||||
parameters: {},
|
||||
uuid: ['testId'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockAlertDetailsDataWithIsObject = mockAlertDetailsData.map((detail) => {
|
||||
return {
|
||||
...detail,
|
||||
isObjectArray: false,
|
||||
};
|
||||
}) as TimelineEventsDetailsItem[];
|
||||
|
||||
jest.mock('../../../../../common/endpoint/service/host_isolation/utils', () => {
|
||||
return {
|
||||
isIsolationSupported: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'../../../../detections/containers/detection_engine/alerts/use_host_isolation_status',
|
||||
() => {
|
||||
return {
|
||||
useHostIsolationStatus: jest.fn().mockReturnValue({
|
||||
loading: false,
|
||||
isIsolated: false,
|
||||
agentStatus: 'healthy',
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock('../../../../common/hooks/use_experimental_features', () => ({
|
||||
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../detections/components/user_info', () => ({
|
||||
useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]),
|
||||
}));
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock(
|
||||
'../../../../detections/containers/detection_engine/alerts/use_alerts_privileges',
|
||||
() => ({
|
||||
useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }),
|
||||
})
|
||||
);
|
||||
jest.mock('../../../../cases/components/use_insert_timeline');
|
||||
|
||||
jest.mock('../../../../common/utils/endpoint_alert_check', () => {
|
||||
return {
|
||||
isAlertFromEndpointAlert: jest.fn().mockReturnValue(true),
|
||||
isAlertFromEndpointEvent: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
});
|
||||
jest.mock(
|
||||
'../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline',
|
||||
() => {
|
||||
return {
|
||||
useInvestigateInTimeline: jest.fn().mockReturnValue({
|
||||
investigateInTimelineActionItems: [<div />],
|
||||
investigateInTimelineAlertClick: () => {},
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
jest.mock('../../../../detections/components/alerts_table/actions');
|
||||
|
||||
const defaultProps = {
|
||||
timelineId: TimelineId.test,
|
||||
loadingEventDetails: false,
|
||||
detailsEcsData: ecsData,
|
||||
isHostIsolationPanelOpen: false,
|
||||
handleOnEventClosed: jest.fn(),
|
||||
onAddIsolationStatusClick: jest.fn(),
|
||||
expandedEvent: { eventId: ecsData._id, indexName: '' },
|
||||
detailsData: mockAlertDetailsDataWithIsObject,
|
||||
};
|
||||
|
||||
describe('event details footer component', () => {
|
||||
beforeEach(() => {
|
||||
const coreStartMock = coreMock.createStart();
|
||||
(KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock);
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
search: {
|
||||
searchStrategyClient: jest.fn(),
|
||||
},
|
||||
query: jest.fn(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('it renders the take action dropdown', () => {
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<EventDetailsFooter {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.getByTestId('take-action-dropdown-btn')).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { find, get, isEmpty } from 'lodash/fp';
|
||||
import { find } from 'lodash/fp';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { TakeActionDropdown } from '../../../../detections/components/take_action_dropdown';
|
||||
import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy';
|
||||
|
@ -19,7 +19,6 @@ import { useEventFilterModal } from '../../../../detections/components/alerts_ta
|
|||
import { getFieldValue } from '../../../../detections/components/host_isolation/helpers';
|
||||
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import { useFetchEcsAlertsData } from '../../../../detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data';
|
||||
import { inputsModel, inputsSelectors, State } from '../../../../common/store';
|
||||
|
||||
interface EventDetailsFooterProps {
|
||||
|
@ -82,11 +81,6 @@ export const EventDetailsFooterComponent = React.memo(
|
|||
[detailsData]
|
||||
);
|
||||
|
||||
const eventIds = useMemo(
|
||||
() => (isEmpty(expandedEvent?.eventId) ? null : [expandedEvent?.eventId]),
|
||||
[expandedEvent?.eventId]
|
||||
);
|
||||
|
||||
const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => {
|
||||
newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)());
|
||||
};
|
||||
|
@ -113,21 +107,15 @@ export const EventDetailsFooterComponent = React.memo(
|
|||
const { closeAddEventFilterModal, isAddEventFilterModalOpen, onAddEventFilterClick } =
|
||||
useEventFilterModal();
|
||||
|
||||
const { alertsEcsData } = useFetchEcsAlertsData({
|
||||
alertIds: eventIds,
|
||||
skip: expandedEvent?.eventId == null,
|
||||
});
|
||||
|
||||
const ecsData = detailsEcsData ?? get(0, alertsEcsData);
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
{ecsData && (
|
||||
{detailsEcsData && (
|
||||
<TakeActionDropdown
|
||||
detailsData={detailsData}
|
||||
ecsData={ecsData}
|
||||
ecsData={detailsEcsData}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
|
||||
loadingEventDetails={loadingEventDetails}
|
||||
|
@ -156,8 +144,8 @@ export const EventDetailsFooterComponent = React.memo(
|
|||
onConfirm={onAddExceptionConfirm}
|
||||
/>
|
||||
)}
|
||||
{isAddEventFilterModalOpen && ecsData != null && (
|
||||
<EventFiltersFlyout data={ecsData} onCancel={closeAddEventFilterModal} />
|
||||
{isAddEventFilterModalOpen && detailsEcsData != null && (
|
||||
<EventFiltersFlyout data={detailsEcsData} onCancel={closeAddEventFilterModal} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -48,7 +48,6 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
ariaRowindex,
|
||||
checked,
|
||||
columnValues,
|
||||
data,
|
||||
ecsData,
|
||||
eventId,
|
||||
eventIdToNoteIds,
|
||||
|
@ -68,7 +67,6 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
|
||||
const emptyNotes: string[] = [];
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const alertIds = useMemo(() => [ecsData._id], [ecsData]);
|
||||
|
||||
const onPinEvent: OnPinEvent = useCallback(
|
||||
(evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })),
|
||||
|
@ -167,7 +165,6 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
<InvestigateInTimelineAction
|
||||
ariaLabel={i18n.SEND_ALERT_TO_TIMELINE_FOR_ROW({ ariaRowindex, columnValues })}
|
||||
key="investigate-in-timeline"
|
||||
alertIds={alertIds}
|
||||
ecsRowData={ecsData}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { EuiCheckbox, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { ALERT_RULE_PRODUCER } from '@kbn/rule-data-utils';
|
||||
import type { ActionProps, HeaderActionProps } from '../../../../../common/types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -19,10 +18,7 @@ export const RowCheckBox = ({
|
|||
columnValues,
|
||||
disabled,
|
||||
loadingEventIds,
|
||||
data,
|
||||
}: ActionProps) => {
|
||||
const ruleProducers = data.find((d) => d.field === ALERT_RULE_PRODUCER)?.value ?? [];
|
||||
const ruleProducer = ruleProducers[0];
|
||||
const handleSelectEvent = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!disabled) {
|
||||
|
@ -39,7 +35,7 @@ export const RowCheckBox = ({
|
|||
<EuiLoadingSpinner size="m" data-test-subj="event-loader" />
|
||||
) : (
|
||||
<EuiCheckbox
|
||||
data-test-subj={`select-event select-event-rule-producer-${ruleProducer}`}
|
||||
data-test-subj={`select-event select-event-${eventId}`}
|
||||
id={eventId}
|
||||
checked={checked && !disabled}
|
||||
disabled={disabled}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue