Skip to content

Commit 295ac7e

Browse files
[Security] Investigate in Resolver Timeline Integration (#70111)
## [Security] `Investigate in Resolver` Timeline Integration This PR adds a new `Investigate in Resolver` action to the Timeline, and all timeline-based views, including: - Timeline - Alert list (i.e. Signals) - Hosts > Events - Hosts > External alerts - Network > External alerts ![investigate-in-resolver-action](https://user-images.githubusercontent.com/4459398/85886173-c40d1c80-b7a2-11ea-8011-0221fef95d51.png) ### Resolver Overlay When the `Investigate in Resolver` action is clicked, Resolver is displayed in an overlay over the events. The screenshot below has placeholder text where Resolver will be rendered: ![resolver-overlay](https://user-images.githubusercontent.com/4459398/85886309-10f0f300-b7a3-11ea-95cb-0117207e4890.png) The Resolver overlay is closed by clicking the `< Back to events` button shown in the screenshot above. The state of the timeline is restored when the overlay is closed. The scroll position (within the events), any expanded events, etc, will appear exactly as they were before the Resolver overlay was displayed. ### Case Integration Users may link directly to a Timeline Resolver view from cases via the `Attach to new case` and `Attach to existing case...` actions show in the screenshot below: ![case-integration](https://user-images.githubusercontent.com/4459398/85886773-e3587980-b7a3-11ea-87b6-b098ea14bc5f.png) ![investigate-in-resolver](https://user-images.githubusercontent.com/4459398/85885618-daff3f00-b7a1-11ea-9356-2e8a1291f213.gif) When users click the link in a case, Timeline will automatically open to the Resolver view in the link. ### URL State Users can directly share Resolver views (in saved Timelines) with other users by copying the Kibana URL to the clipboard when Resolver is open. When another user pastes the URL in their browser, Timeline will automatically open and display the Resolver view in the URL. ### Enabling the `Investigate in Resolver` action In this PR, the `Investigate in Resolver` action is only enabled for events where all of the following are true: - `agent.type` is `endpoint` - `process.entity_id` exists ### Context passed to Resolver The only context passed to `Resolver` is the `_id` of the event (when the user clicks `Investigate in Resolver`) ### What's next? - @oatkiller will replace the placeholder text shown in the screenshots above with the actual call to Resolver in a separate PR - I will follow-up this PR with additional tests - The action text `Investigate in Resolver` may be changed in a future PR - Hide the `Add to case` action in timeline-based views (it's currently visible, but disabled)
1 parent 59925da commit 295ac7e

File tree

57 files changed

+1615
-1024
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1615
-1024
lines changed

x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import React from 'react';
1010
import ApolloClient from 'apollo-client';
11+
import { Dispatch } from 'redux';
1112

1213
import { EuiText } from '@elastic/eui';
1314
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
@@ -17,10 +18,12 @@ import {
1718
TimelineRowActionOnClick,
1819
} from '../../../timelines/components/timeline/body/actions';
1920
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
21+
import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
2022
import {
2123
DEFAULT_COLUMN_MIN_WIDTH,
2224
DEFAULT_DATE_COLUMN_MIN_WIDTH,
2325
} from '../../../timelines/components/timeline/body/constants';
26+
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../timelines/components/timeline/helpers';
2427
import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model';
2528
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
2629

@@ -174,23 +177,27 @@ export const getAlertActions = ({
174177
apolloClient,
175178
canUserCRUD,
176179
createTimeline,
180+
dispatch,
177181
hasIndexWrite,
178182
onAlertStatusUpdateFailure,
179183
onAlertStatusUpdateSuccess,
180184
setEventsDeleted,
181185
setEventsLoading,
182186
status,
187+
timelineId,
183188
updateTimelineIsLoading,
184189
}: {
185190
apolloClient?: ApolloClient<{}>;
186191
canUserCRUD: boolean;
187192
createTimeline: CreateTimeline;
193+
dispatch: Dispatch;
188194
hasIndexWrite: boolean;
189195
onAlertStatusUpdateFailure: (status: Status, error: Error) => void;
190196
onAlertStatusUpdateSuccess: (count: number, status: Status) => void;
191197
setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void;
192198
setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void;
193199
status: Status;
200+
timelineId: string;
194201
updateTimelineIsLoading: UpdateTimelineLoading;
195202
}): TimelineRowAction[] => {
196203
const openAlertActionComponent: TimelineRowAction = {
@@ -199,7 +206,7 @@ export const getAlertActions = ({
199206
dataTestSubj: 'open-alert-status',
200207
displayType: 'contextMenu',
201208
id: FILTER_OPEN,
202-
isActionDisabled: !canUserCRUD || !hasIndexWrite,
209+
isActionDisabled: () => !canUserCRUD || !hasIndexWrite,
203210
onClick: ({ eventId }: TimelineRowActionOnClick) =>
204211
updateAlertStatusAction({
205212
alertIds: [eventId],
@@ -210,7 +217,7 @@ export const getAlertActions = ({
210217
status,
211218
selectedStatus: FILTER_OPEN,
212219
}),
213-
width: 26,
220+
width: DEFAULT_ICON_BUTTON_WIDTH,
214221
};
215222

216223
const closeAlertActionComponent: TimelineRowAction = {
@@ -219,7 +226,7 @@ export const getAlertActions = ({
219226
dataTestSubj: 'close-alert-status',
220227
displayType: 'contextMenu',
221228
id: FILTER_CLOSED,
222-
isActionDisabled: !canUserCRUD || !hasIndexWrite,
229+
isActionDisabled: () => !canUserCRUD || !hasIndexWrite,
223230
onClick: ({ eventId }: TimelineRowActionOnClick) =>
224231
updateAlertStatusAction({
225232
alertIds: [eventId],
@@ -230,7 +237,7 @@ export const getAlertActions = ({
230237
status,
231238
selectedStatus: FILTER_CLOSED,
232239
}),
233-
width: 26,
240+
width: DEFAULT_ICON_BUTTON_WIDTH,
234241
};
235242

236243
const inProgressAlertActionComponent: TimelineRowAction = {
@@ -239,7 +246,7 @@ export const getAlertActions = ({
239246
dataTestSubj: 'in-progress-alert-status',
240247
displayType: 'contextMenu',
241248
id: FILTER_IN_PROGRESS,
242-
isActionDisabled: !canUserCRUD || !hasIndexWrite,
249+
isActionDisabled: () => !canUserCRUD || !hasIndexWrite,
243250
onClick: ({ eventId }: TimelineRowActionOnClick) =>
244251
updateAlertStatusAction({
245252
alertIds: [eventId],
@@ -250,10 +257,13 @@ export const getAlertActions = ({
250257
status,
251258
selectedStatus: FILTER_IN_PROGRESS,
252259
}),
253-
width: 26,
260+
width: DEFAULT_ICON_BUTTON_WIDTH,
254261
};
255262

256263
return [
264+
{
265+
...getInvestigateInResolverAction({ dispatch, timelineId }),
266+
},
257267
{
258268
ariaLabel: 'Send alert to timeline',
259269
content: i18n.ACTION_INVESTIGATE_IN_TIMELINE,
@@ -268,7 +278,7 @@ export const getAlertActions = ({
268278
ecsData,
269279
updateTimelineIsLoading,
270280
}),
271-
width: 26,
281+
width: DEFAULT_ICON_BUTTON_WIDTH,
272282
},
273283
// Context menu items
274284
...(FILTER_OPEN !== status ? [openAlertActionComponent] : []),

x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,40 @@
77
import React from 'react';
88
import { shallow } from 'enzyme';
99

10+
import { TestProviders } from '../../../common/mock/test_providers';
1011
import { TimelineId } from '../../../../common/types/timeline';
1112
import { AlertsTableComponent } from './index';
1213

1314
describe('AlertsTableComponent', () => {
1415
it('renders correctly', () => {
1516
const wrapper = shallow(
16-
<AlertsTableComponent
17-
timelineId={TimelineId.test}
18-
canUserCRUD
19-
hasIndexWrite
20-
from={0}
21-
loading
22-
signalsIndex="index"
23-
to={1}
24-
globalQuery={{
25-
query: 'query',
26-
language: 'language',
27-
}}
28-
globalFilters={[]}
29-
deletedEventIds={[]}
30-
loadingEventIds={[]}
31-
selectedEventIds={{}}
32-
isSelectAllChecked={false}
33-
clearSelected={jest.fn()}
34-
setEventsLoading={jest.fn()}
35-
clearEventsLoading={jest.fn()}
36-
setEventsDeleted={jest.fn()}
37-
clearEventsDeleted={jest.fn()}
38-
updateTimelineIsLoading={jest.fn()}
39-
updateTimeline={jest.fn()}
40-
/>
17+
<TestProviders>
18+
<AlertsTableComponent
19+
timelineId={TimelineId.test}
20+
canUserCRUD
21+
hasIndexWrite
22+
from={0}
23+
loading
24+
signalsIndex="index"
25+
to={1}
26+
globalQuery={{
27+
query: 'query',
28+
language: 'language',
29+
}}
30+
globalFilters={[]}
31+
deletedEventIds={[]}
32+
loadingEventIds={[]}
33+
selectedEventIds={{}}
34+
isSelectAllChecked={false}
35+
clearSelected={jest.fn()}
36+
setEventsLoading={jest.fn()}
37+
clearEventsLoading={jest.fn()}
38+
setEventsDeleted={jest.fn()}
39+
clearEventsDeleted={jest.fn()}
40+
updateTimelineIsLoading={jest.fn()}
41+
updateTimeline={jest.fn()}
42+
/>
43+
</TestProviders>
4144
);
4245

4346
expect(wrapper.find('[title="Alerts"]')).toBeTruthy();

x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { EuiPanel, EuiLoadingContent } from '@elastic/eui';
88
import { isEmpty } from 'lodash/fp';
99
import React, { useCallback, useEffect, useMemo, useState } from 'react';
10-
import { connect, ConnectedProps } from 'react-redux';
10+
import { connect, ConnectedProps, useDispatch } from 'react-redux';
1111
import { Dispatch } from 'redux';
1212

1313
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
@@ -84,6 +84,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
8484
updateTimeline,
8585
updateTimelineIsLoading,
8686
}) => {
87+
const dispatch = useDispatch();
8788
const [selectAll, setSelectAll] = useState(false);
8889
const apolloClient = useApolloClient();
8990

@@ -292,11 +293,13 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
292293
getAlertActions({
293294
apolloClient,
294295
canUserCRUD,
296+
dispatch,
295297
hasIndexWrite,
296298
createTimeline: createTimelineCallback,
297299
setEventsLoading: setEventsLoadingCallback,
298300
setEventsDeleted: setEventsDeletedCallback,
299301
status: filterGroup,
302+
timelineId,
300303
updateTimelineIsLoading,
301304
onAlertStatusUpdateSuccess,
302305
onAlertStatusUpdateFailure,
@@ -305,10 +308,12 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
305308
apolloClient,
306309
canUserCRUD,
307310
createTimelineCallback,
311+
dispatch,
308312
hasIndexWrite,
309313
filterGroup,
310314
setEventsLoadingCallback,
311315
setEventsDeletedCallback,
316+
timelineId,
312317
updateTimelineIsLoading,
313318
onAlertStatusUpdateSuccess,
314319
onAlertStatusUpdateFailure,

x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
*/
66

77
import React, { useEffect, useMemo } from 'react';
8+
import { useDispatch } from 'react-redux';
89

910
import { Filter } from '../../../../../../../src/plugins/data/public';
1011
import { TimelineIdLiteral } from '../../../../common/types/timeline';
1112
import { StatefulEventsViewer } from '../events_viewer';
1213
import { alertsDefaultModel } from './default_headers';
1314
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
15+
import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
1416
import * as i18n from './translations';
17+
1518
export interface OwnProps {
1619
end: number;
1720
id: string;
@@ -64,8 +67,9 @@ const AlertsTableComponent: React.FC<Props> = ({
6467
startDate,
6568
pageFilters = [],
6669
}) => {
70+
const dispatch = useDispatch();
6771
const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]);
68-
const { initializeTimeline } = useManageTimeline();
72+
const { initializeTimeline, setTimelineRowActions } = useManageTimeline();
6973

7074
useEffect(() => {
7175
initializeTimeline({
@@ -75,6 +79,10 @@ const AlertsTableComponent: React.FC<Props> = ({
7579
title: i18n.ALERTS_TABLE_TITLE,
7680
unit: i18n.UNIT,
7781
});
82+
setTimelineRowActions({
83+
id: timelineId,
84+
timelineRowActions: [getInvestigateInResolverAction({ dispatch, timelineId })],
85+
});
7886
// eslint-disable-next-line react-hooks/exhaustive-deps
7987
}, []);
8088
return (

x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { EuiPanel } from '@elastic/eui';
88
import { getOr, isEmpty, union } from 'lodash/fp';
99
import React, { useEffect, useMemo, useState } from 'react';
10+
import { useDispatch } from 'react-redux';
1011
import styled from 'styled-components';
1112
import deepEqual from 'fast-deep-equal';
1213

@@ -34,6 +35,7 @@ import {
3435
} from '../../../../../../../src/plugins/data/public';
3536
import { inputsModel } from '../../store';
3637
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
38+
import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
3739

3840
const DEFAULT_EVENTS_VIEWER_HEIGHT = 500;
3941

@@ -91,6 +93,7 @@ const EventsViewerComponent: React.FC<Props> = ({
9193
toggleColumn,
9294
utilityBar,
9395
}) => {
96+
const dispatch = useDispatch();
9497
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
9598
const kibana = useKibana();
9699
const { filterManager } = useKibana().services.data.query;
@@ -100,7 +103,16 @@ const EventsViewerComponent: React.FC<Props> = ({
100103
getManageTimelineById,
101104
setIsTimelineLoading,
102105
setTimelineFilterManager,
106+
setTimelineRowActions,
103107
} = useManageTimeline();
108+
109+
useEffect(() => {
110+
setTimelineRowActions({
111+
id,
112+
timelineRowActions: [getInvestigateInResolverAction({ dispatch, timelineId: id })],
113+
});
114+
}, [setTimelineRowActions, id, dispatch]);
115+
104116
useEffect(() => {
105117
setIsTimelineLoading({ id, isLoading: isQueryLoading });
106118
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -179,9 +191,7 @@ const EventsViewerComponent: React.FC<Props> = ({
179191
<HeaderSection id={id} subtitle={utilityBar ? undefined : subtitle} title={title}>
180192
{headerFilterGroup}
181193
</HeaderSection>
182-
183194
{utilityBar?.(refetch, totalCountMinusDeleted)}
184-
185195
<EventsContainerLoading data-test-subj={`events-container-loading-${loading}`}>
186196
<TimelineRefetch
187197
id={id}

x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const getMockObject = (
7474
timeline: {
7575
id: '',
7676
isOpen: false,
77+
graphEventId: '',
7778
},
7879
timerange: {
7980
global: {

x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ describe('SIEM Navigation', () => {
8181
[CONSTANTS.timeline]: {
8282
id: '',
8383
isOpen: false,
84+
graphEventId: '',
8485
},
8586
},
8687
};
@@ -160,6 +161,7 @@ describe('SIEM Navigation', () => {
160161
timeline: {
161162
id: '',
162163
isOpen: false,
164+
graphEventId: '',
163165
},
164166
timerange: {
165167
global: {
@@ -266,7 +268,7 @@ describe('SIEM Navigation', () => {
266268
search: '',
267269
state: undefined,
268270
tabName: 'authentications',
269-
timeline: { id: '', isOpen: false },
271+
timeline: { id: '', isOpen: false, graphEventId: '' },
270272
timerange: {
271273
global: {
272274
linkTo: ['timeline'],

x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ describe('Tab Navigation', () => {
7171
[CONSTANTS.timeline]: {
7272
id: '',
7373
isOpen: false,
74+
graphEventId: '',
7475
},
7576
};
7677
test('it mounts with correct tab highlighted', () => {
@@ -128,6 +129,7 @@ describe('Tab Navigation', () => {
128129
[CONSTANTS.timeline]: {
129130
id: '',
130131
isOpen: false,
132+
graphEventId: '',
131133
},
132134
};
133135
test('it mounts with correct tab highlighted', () => {

x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ export const makeMapStateToProps = () => {
126126
? {
127127
id: flyoutTimeline.savedObjectId != null ? flyoutTimeline.savedObjectId : '',
128128
isOpen: flyoutTimeline.show,
129+
graphEventId: flyoutTimeline.graphEventId ?? '',
129130
}
130-
: { id: '', isOpen: false };
131+
: { id: '', isOpen: false, graphEventId: '' };
131132

132133
let searchAttr: {
133134
[CONSTANTS.appQuery]?: Query;

x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export const dispatchSetInitialStateFromUrl = (
8181
queryTimelineById({
8282
apolloClient,
8383
duplicate: false,
84+
graphEventId: timeline.graphEventId,
8485
timelineId: timeline.id,
8586
openTimeline: timeline.isOpen,
8687
updateIsLoading: updateTimelineIsLoading,

0 commit comments

Comments
 (0)