diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx
index 2029c5169c2cdc..6d82897aaf0102 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx
+++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import ApolloClient from 'apollo-client';
+import { Dispatch } from 'redux';
import { EuiText } from '@elastic/eui';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
@@ -17,10 +18,12 @@ import {
TimelineRowActionOnClick,
} from '../../../timelines/components/timeline/body/actions';
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
+import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
} from '../../../timelines/components/timeline/body/constants';
+import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../timelines/components/timeline/helpers';
import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
@@ -174,23 +177,27 @@ export const getAlertActions = ({
apolloClient,
canUserCRUD,
createTimeline,
+ dispatch,
hasIndexWrite,
onAlertStatusUpdateFailure,
onAlertStatusUpdateSuccess,
setEventsDeleted,
setEventsLoading,
status,
+ timelineId,
updateTimelineIsLoading,
}: {
apolloClient?: ApolloClient<{}>;
canUserCRUD: boolean;
createTimeline: CreateTimeline;
+ dispatch: Dispatch;
hasIndexWrite: boolean;
onAlertStatusUpdateFailure: (status: Status, error: Error) => void;
onAlertStatusUpdateSuccess: (count: number, status: Status) => void;
setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void;
setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void;
status: Status;
+ timelineId: string;
updateTimelineIsLoading: UpdateTimelineLoading;
}): TimelineRowAction[] => {
const openAlertActionComponent: TimelineRowAction = {
@@ -199,7 +206,7 @@ export const getAlertActions = ({
dataTestSubj: 'open-alert-status',
displayType: 'contextMenu',
id: FILTER_OPEN,
- isActionDisabled: !canUserCRUD || !hasIndexWrite,
+ isActionDisabled: () => !canUserCRUD || !hasIndexWrite,
onClick: ({ eventId }: TimelineRowActionOnClick) =>
updateAlertStatusAction({
alertIds: [eventId],
@@ -210,7 +217,7 @@ export const getAlertActions = ({
status,
selectedStatus: FILTER_OPEN,
}),
- width: 26,
+ width: DEFAULT_ICON_BUTTON_WIDTH,
};
const closeAlertActionComponent: TimelineRowAction = {
@@ -219,7 +226,7 @@ export const getAlertActions = ({
dataTestSubj: 'close-alert-status',
displayType: 'contextMenu',
id: FILTER_CLOSED,
- isActionDisabled: !canUserCRUD || !hasIndexWrite,
+ isActionDisabled: () => !canUserCRUD || !hasIndexWrite,
onClick: ({ eventId }: TimelineRowActionOnClick) =>
updateAlertStatusAction({
alertIds: [eventId],
@@ -230,7 +237,7 @@ export const getAlertActions = ({
status,
selectedStatus: FILTER_CLOSED,
}),
- width: 26,
+ width: DEFAULT_ICON_BUTTON_WIDTH,
};
const inProgressAlertActionComponent: TimelineRowAction = {
@@ -239,7 +246,7 @@ export const getAlertActions = ({
dataTestSubj: 'in-progress-alert-status',
displayType: 'contextMenu',
id: FILTER_IN_PROGRESS,
- isActionDisabled: !canUserCRUD || !hasIndexWrite,
+ isActionDisabled: () => !canUserCRUD || !hasIndexWrite,
onClick: ({ eventId }: TimelineRowActionOnClick) =>
updateAlertStatusAction({
alertIds: [eventId],
@@ -250,10 +257,13 @@ export const getAlertActions = ({
status,
selectedStatus: FILTER_IN_PROGRESS,
}),
- width: 26,
+ width: DEFAULT_ICON_BUTTON_WIDTH,
};
return [
+ {
+ ...getInvestigateInResolverAction({ dispatch, timelineId }),
+ },
{
ariaLabel: 'Send alert to timeline',
content: i18n.ACTION_INVESTIGATE_IN_TIMELINE,
@@ -268,7 +278,7 @@ export const getAlertActions = ({
ecsData,
updateTimelineIsLoading,
}),
- width: 26,
+ width: DEFAULT_ICON_BUTTON_WIDTH,
},
// Context menu items
...(FILTER_OPEN !== status ? [openAlertActionComponent] : []),
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx
index f843bf68818465..9ff368aff2bf68 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx
@@ -7,37 +7,40 @@
import React from 'react';
import { shallow } from 'enzyme';
+import { TestProviders } from '../../../common/mock/test_providers';
import { TimelineId } from '../../../../common/types/timeline';
import { AlertsTableComponent } from './index';
describe('AlertsTableComponent', () => {
it('renders correctly', () => {
const wrapper = shallow(
-
+
+
+
);
expect(wrapper.find('[title="Alerts"]')).toBeTruthy();
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx
index ba6102312fef67..ec088c111e3bbc 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx
@@ -7,7 +7,7 @@
import { EuiPanel, EuiLoadingContent } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { connect, ConnectedProps } from 'react-redux';
+import { connect, ConnectedProps, useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
@@ -84,6 +84,7 @@ export const AlertsTableComponent: React.FC = ({
updateTimeline,
updateTimelineIsLoading,
}) => {
+ const dispatch = useDispatch();
const [selectAll, setSelectAll] = useState(false);
const apolloClient = useApolloClient();
@@ -292,11 +293,13 @@ export const AlertsTableComponent: React.FC = ({
getAlertActions({
apolloClient,
canUserCRUD,
+ dispatch,
hasIndexWrite,
createTimeline: createTimelineCallback,
setEventsLoading: setEventsLoadingCallback,
setEventsDeleted: setEventsDeletedCallback,
status: filterGroup,
+ timelineId,
updateTimelineIsLoading,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
@@ -305,10 +308,12 @@ export const AlertsTableComponent: React.FC = ({
apolloClient,
canUserCRUD,
createTimelineCallback,
+ dispatch,
hasIndexWrite,
filterGroup,
setEventsLoadingCallback,
setEventsDeletedCallback,
+ timelineId,
updateTimelineIsLoading,
onAlertStatusUpdateSuccess,
onAlertStatusUpdateFailure,
diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
index 251e0278b11bab..6d5471404ab4d1 100644
--- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
@@ -5,13 +5,16 @@
*/
import React, { useEffect, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
import { Filter } from '../../../../../../../src/plugins/data/public';
import { TimelineIdLiteral } from '../../../../common/types/timeline';
import { StatefulEventsViewer } from '../events_viewer';
import { alertsDefaultModel } from './default_headers';
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
+import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
import * as i18n from './translations';
+
export interface OwnProps {
end: number;
id: string;
@@ -64,8 +67,9 @@ const AlertsTableComponent: React.FC = ({
startDate,
pageFilters = [],
}) => {
+ const dispatch = useDispatch();
const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]);
- const { initializeTimeline } = useManageTimeline();
+ const { initializeTimeline, setTimelineRowActions } = useManageTimeline();
useEffect(() => {
initializeTimeline({
@@ -75,6 +79,10 @@ const AlertsTableComponent: React.FC = ({
title: i18n.ALERTS_TABLE_TITLE,
unit: i18n.UNIT,
});
+ setTimelineRowActions({
+ id: timelineId,
+ timelineRowActions: [getInvestigateInResolverAction({ dispatch, timelineId })],
+ });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
index 6b4baac0ff26c4..9e38b14c4334a5 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
@@ -7,6 +7,7 @@
import { EuiPanel } from '@elastic/eui';
import { getOr, isEmpty, union } from 'lodash/fp';
import React, { useEffect, useMemo, useState } from 'react';
+import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
@@ -34,6 +35,7 @@ import {
} from '../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../store';
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
+import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
const DEFAULT_EVENTS_VIEWER_HEIGHT = 500;
@@ -91,6 +93,7 @@ const EventsViewerComponent: React.FC = ({
toggleColumn,
utilityBar,
}) => {
+ const dispatch = useDispatch();
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
const kibana = useKibana();
const { filterManager } = useKibana().services.data.query;
@@ -100,7 +103,16 @@ const EventsViewerComponent: React.FC = ({
getManageTimelineById,
setIsTimelineLoading,
setTimelineFilterManager,
+ setTimelineRowActions,
} = useManageTimeline();
+
+ useEffect(() => {
+ setTimelineRowActions({
+ id,
+ timelineRowActions: [getInvestigateInResolverAction({ dispatch, timelineId: id })],
+ });
+ }, [setTimelineRowActions, id, dispatch]);
+
useEffect(() => {
setIsTimelineLoading({ id, isLoading: isQueryLoading });
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -179,9 +191,7 @@ const EventsViewerComponent: React.FC = ({
{headerFilterGroup}
-
{utilityBar?.(refetch, totalCountMinusDeleted)}
-
{
[CONSTANTS.timeline]: {
id: '',
isOpen: false,
+ graphEventId: '',
},
},
};
@@ -160,6 +161,7 @@ describe('SIEM Navigation', () => {
timeline: {
id: '',
isOpen: false,
+ graphEventId: '',
},
timerange: {
global: {
@@ -266,7 +268,7 @@ describe('SIEM Navigation', () => {
search: '',
state: undefined,
tabName: 'authentications',
- timeline: { id: '', isOpen: false },
+ timeline: { id: '', isOpen: false, graphEventId: '' },
timerange: {
global: {
linkTo: ['timeline'],
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx
index 977c7808b6c86e..f345346d620cb1 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx
@@ -71,6 +71,7 @@ describe('Tab Navigation', () => {
[CONSTANTS.timeline]: {
id: '',
isOpen: false,
+ graphEventId: '',
},
};
test('it mounts with correct tab highlighted', () => {
@@ -128,6 +129,7 @@ describe('Tab Navigation', () => {
[CONSTANTS.timeline]: {
id: '',
isOpen: false,
+ graphEventId: '',
},
};
test('it mounts with correct tab highlighted', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
index c270a99d3c51e5..7f4267bc5e2b34 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
@@ -126,8 +126,9 @@ export const makeMapStateToProps = () => {
? {
id: flyoutTimeline.savedObjectId != null ? flyoutTimeline.savedObjectId : '',
isOpen: flyoutTimeline.show,
+ graphEventId: flyoutTimeline.graphEventId ?? '',
}
- : { id: '', isOpen: false };
+ : { id: '', isOpen: false, graphEventId: '' };
let searchAttr: {
[CONSTANTS.appQuery]?: Query;
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx
index efd6221bbfbd02..ab03e2199474c6 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx
@@ -81,6 +81,7 @@ export const dispatchSetInitialStateFromUrl = (
queryTimelineById({
apolloClient,
duplicate: false,
+ graphEventId: timeline.graphEventId,
timelineId: timeline.id,
openTimeline: timeline.isOpen,
updateIsLoading: updateTimelineIsLoading,
diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json
index 3c8c7c21d72a02..48547212bb6c01 100644
--- a/x-pack/plugins/security_solution/public/graphql/introspection.json
+++ b/x-pack/plugins/security_solution/public/graphql/introspection.json
@@ -3570,6 +3570,14 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "agent",
+ "description": "",
+ "args": [],
+ "type": { "kind": "OBJECT", "name": "AgentEcsField", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "auditd",
"description": "",
@@ -3760,6 +3768,25 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "OBJECT",
+ "name": "AgentEcsField",
+ "description": "",
+ "fields": [
+ {
+ "name": "type",
+ "description": "",
+ "args": [],
+ "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [],
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "OBJECT",
"name": "AuditdEcsFields",
@@ -5728,6 +5755,14 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "entity_id",
+ "description": "",
+ "args": [],
+ "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "executable",
"description": "",
diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts
index dc4a8ae78bf46d..b5088fe51b446b 100644
--- a/x-pack/plugins/security_solution/public/graphql/types.ts
+++ b/x-pack/plugins/security_solution/public/graphql/types.ts
@@ -763,6 +763,8 @@ export interface Ecs {
_index?: Maybe;
+ agent?: Maybe;
+
auditd?: Maybe;
destination?: Maybe;
@@ -810,6 +812,10 @@ export interface Ecs {
system?: Maybe;
}
+export interface AgentEcsField {
+ type?: Maybe;
+}
+
export interface AuditdEcsFields {
result?: Maybe;
@@ -1265,6 +1271,8 @@ export interface ProcessEcsFields {
args?: Maybe;
+ entity_id?: Maybe;
+
executable?: Maybe;
title?: Maybe;
@@ -4605,6 +4613,8 @@ export namespace GetTimelineQuery {
event: Maybe;
+ agent: Maybe;
+
auditd: Maybe;
file: Maybe;
@@ -4730,6 +4740,12 @@ export namespace GetTimelineQuery {
type: Maybe;
};
+ export type Agent = {
+ __typename?: 'AgentEcsField';
+
+ type: Maybe;
+ };
+
export type Auditd = {
__typename?: 'AuditdEcsFields';
@@ -5155,6 +5171,8 @@ export namespace GetTimelineQuery {
args: Maybe;
+ entity_id: Maybe;
+
executable: Maybe;
title: Maybe;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
index 480070fda9594e..7addfaaf7c5fce 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
@@ -32,6 +32,10 @@ const Title = styled(EuiTitle)`
padding-left: 5px;
`;
+const H5 = styled.h5`
+ text-align: left;
+`;
+
Title.displayName = 'Title';
type Props = Pick & {
@@ -64,7 +68,7 @@ export const CategoriesPane = React.memo(
}) => (
<>
- {i18n.CATEGORIES}
+ {i18n.CATEGORIES}
`
border: ${({ theme }) => theme.eui.euiBorderWidthThin} solid
${({ theme }) => theme.eui.euiColorMediumShade};
border-radius: ${({ theme }) => theme.eui.euiBorderRadius};
- left: 0;
+ left: 8px;
padding: ${({ theme }) => theme.eui.paddingSizes.s} ${({ theme }) => theme.eui.paddingSizes.s}
- ${({ theme }) => theme.eui.paddingSizes.m};
+ ${({ theme }) => theme.eui.paddingSizes.s};
position: absolute;
- top: calc(100% + ${({ theme }) => theme.eui.euiSize});
+ top: calc(100% + 4px);
width: ${({ width }) => width}px;
z-index: 9990;
`;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
index a3e93ff3c90eb9..a3937107936b68 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
@@ -26,6 +26,7 @@ export const INPUT_TIMEOUT = 250;
const FieldsBrowserButtonContainer = styled.div`
position: relative;
+ width: 24px;
`;
FieldsBrowserButtonContainer.displayName = 'FieldsBrowserButtonContainer';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
index 8ad32d6e2cad01..9fe48cd2f0190b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
@@ -33,6 +33,7 @@ const StatefulFlyoutHeader = React.memo(
associateNote,
createTimeline,
description,
+ graphEventId,
isDataInTimeline,
isDatepickerLocked,
isFavorite,
@@ -58,6 +59,7 @@ const StatefulFlyoutHeader = React.memo(
createTimeline={createTimeline}
description={description}
getNotesByIds={getNotesByIds}
+ graphEventId={graphEventId}
isDataInTimeline={isDataInTimeline}
isDatepickerLocked={isDatepickerLocked}
isFavorite={isFavorite}
@@ -92,6 +94,7 @@ const makeMapStateToProps = () => {
const {
dataProviders,
description = '',
+ graphEventId,
isFavorite = false,
kqlQuery,
title = '',
@@ -103,13 +106,14 @@ const makeMapStateToProps = () => {
return {
description,
- notesById: getNotesByIds(state),
+ graphEventId,
history,
isDataInTimeline:
!isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)),
isFavorite,
isDatepickerLocked: globalInput.linkTo.includes('timeline'),
noteIds,
+ notesById: getNotesByIds(state),
status,
title,
};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx
new file mode 100644
index 00000000000000..fe38dd79176a5c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx
@@ -0,0 +1,150 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiTitle,
+} from '@elastic/eui';
+import { noop } from 'lodash/fp';
+import React, { useCallback, useState } from 'react';
+import { connect, ConnectedProps, useDispatch, useSelector } from 'react-redux';
+import styled from 'styled-components';
+
+import { SecurityPageName } from '../../../app/types';
+import { AllCasesModal } from '../../../cases/components/all_cases_modal';
+import { getCaseDetailsUrl } from '../../../common/components/link_to';
+import { APP_ID } from '../../../../common/constants';
+import { useKibana } from '../../../common/lib/kibana';
+import { State } from '../../../common/store';
+import { timelineSelectors } from '../../store/timeline';
+import { timelineDefaults } from '../../store/timeline/defaults';
+import { TimelineModel } from '../../store/timeline/model';
+import { NewCase, ExistingCase } from '../timeline/properties/helpers';
+import { UNTITLED_TIMELINE } from '../timeline/properties/translations';
+import {
+ setInsertTimeline,
+ updateTimelineGraphEventId,
+} from '../../../timelines/store/timeline/actions';
+
+import * as i18n from './translations';
+
+const OverlayContainer = styled.div<{ bodyHeight?: number }>`
+ height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')};
+ width: 100%;
+`;
+
+interface OwnProps {
+ bodyHeight?: number;
+ graphEventId?: string;
+ timelineId: string;
+}
+
+const GraphOverlayComponent = ({
+ bodyHeight,
+ graphEventId,
+ status,
+ timelineId,
+ title,
+}: OwnProps & PropsFromRedux) => {
+ const dispatch = useDispatch();
+ const { navigateToApp } = useKibana().services.application;
+ const onCloseOverlay = useCallback(() => {
+ dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' }));
+ }, [dispatch, timelineId]);
+ const [showCaseModal, setShowCaseModal] = useState(false);
+ const onOpenCaseModal = useCallback(() => setShowCaseModal(true), []);
+ const onCloseCaseModal = useCallback(() => setShowCaseModal(false), [setShowCaseModal]);
+ const currentTimeline = useSelector((state: State) =>
+ timelineSelectors.selectTimeline(state, timelineId)
+ );
+ const onRowClick = useCallback(
+ (id: string) => {
+ onCloseCaseModal();
+
+ dispatch(
+ setInsertTimeline({
+ graphEventId,
+ timelineId,
+ timelineSavedObjectId: currentTimeline.savedObjectId,
+ timelineTitle: title.length > 0 ? title : UNTITLED_TIMELINE,
+ })
+ );
+
+ navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
+ path: getCaseDetailsUrl({ id }),
+ });
+ },
+ [currentTimeline, dispatch, graphEventId, navigateToApp, onCloseCaseModal, timelineId, title]
+ );
+
+ return (
+
+
+
+
+
+ {i18n.BACK_TO_EVENTS}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <>{`Resolver graph for event _id ${graphEventId}`}>
+
+
+
+ );
+};
+
+const makeMapStateToProps = () => {
+ const getTimeline = timelineSelectors.getTimelineByIdSelector();
+ const mapStateToProps = (state: State, { timelineId }: OwnProps) => {
+ const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
+ const { status, title = '' } = timeline;
+
+ return {
+ status,
+ title,
+ };
+ };
+ return mapStateToProps;
+};
+
+const connector = connect(makeMapStateToProps);
+
+type PropsFromRedux = ConnectedProps;
+
+export const GraphOverlay = connector(GraphOverlayComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts
new file mode 100644
index 00000000000000..c7cd9253de0383
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const BACK_TO_EVENTS = i18n.translate(
+ 'xpack.securitySolution.timeline.graphOverlay.backToEventsButton',
+ {
+ defaultMessage: '< Back to events',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
index c8a47798f169ce..520215cde4862c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
@@ -190,6 +190,7 @@ export const formatTimelineResultToModel = (
export interface QueryTimelineById {
apolloClient: ApolloClient | ApolloClient<{}> | undefined;
duplicate?: boolean;
+ graphEventId?: string;
timelineId: string;
onOpenTimeline?: (timeline: TimelineModel) => void;
openTimeline?: boolean;
@@ -206,6 +207,7 @@ export interface QueryTimelineById {
export const queryTimelineById = ({
apolloClient,
duplicate = false,
+ graphEventId = '',
timelineId,
onOpenTimeline,
openTimeline = true,
@@ -238,6 +240,7 @@ export const queryTimelineById = ({
notes,
timeline: {
...timeline,
+ graphEventId,
show: openTimeline,
},
to: getOr(to, 'dateRange.end', timeline),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
index 4e6cce618880b1..92782252719303 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
@@ -1,882 +1,942 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Timeline rendering renders correctly against snapshot 1`] = `
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
+ Object {
+ "aggregatable": true,
+ "category": "host",
+ "columnHeaderType": "not-filtered",
+ "description": "Name of the host.
+It can contain what \`hostname\` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.",
+ "example": "",
+ "id": "host.name",
+ "type": "keyword",
+ "width": 180,
+ },
+ Object {
+ "aggregatable": true,
+ "category": "source",
+ "columnHeaderType": "not-filtered",
+ "description": "IP address of the source.
+Can be one or multiple IPv4 or IPv6 addresses.",
+ "example": "",
+ "id": "source.ip",
+ "type": "ip",
+ "width": 180,
+ },
+ Object {
+ "aggregatable": true,
+ "category": "destination",
+ "columnHeaderType": "not-filtered",
+ "description": "IP address of the destination.
+Can be one or multiple IPv4 or IPv6 addresses.",
+ "example": "",
+ "id": "destination.ip",
+ "type": "ip",
+ "width": 180,
+ },
+ Object {
+ "aggregatable": true,
+ "category": "destination",
+ "columnHeaderType": "not-filtered",
+ "description": "Bytes sent from the source to the destination",
+ "example": "123",
+ "format": "bytes",
+ "id": "destination.bytes",
+ "type": "number",
+ "width": 180,
+ },
+ Object {
+ "aggregatable": true,
+ "category": "user",
+ "columnHeaderType": "not-filtered",
+ "description": "Short name or login of the user.",
+ "example": "albert",
+ "id": "user.name",
+ "type": "keyword",
+ "width": 180,
+ },
+ Object {
+ "aggregatable": true,
+ "category": "base",
+ "columnHeaderType": "not-filtered",
+ "description": "Each document has an _id that uniquely identifies it",
+ "example": "Y-6TfmcB0WOhS6qyMv3s",
+ "id": "_id",
+ "type": "keyword",
+ "width": 180,
+ },
+ Object {
+ "aggregatable": false,
+ "category": "base",
+ "columnHeaderType": "not-filtered",
+ "description": "For log events the message field contains the log message.
+In other use cases the message field can be used to concatenate different values which are then freely searchable. If multiple messages exist, they can be combined into one message.",
+ "example": "Hello World",
+ "id": "message",
+ "type": "text",
+ "width": 180,
+ },
+ ]
+ }
+ dataProviders={
+ Array [
+ Object {
+ "and": Array [
+ Object {
+ "and": Array [],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 2",
+ "kqlQuery": "",
+ "name": "Provider 2",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 2",
+ },
+ },
+ Object {
+ "and": Array [],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 3",
+ "kqlQuery": "",
+ "name": "Provider 3",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 3",
+ },
+ },
+ ],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 1",
+ "kqlQuery": "",
+ "name": "Provider 1",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 1",
+ },
+ },
+ Object {
+ "and": Array [],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 2",
+ "kqlQuery": "",
+ "name": "Provider 2",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 2",
+ },
+ },
+ Object {
+ "and": Array [],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 3",
+ "kqlQuery": "",
+ "name": "Provider 3",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 3",
+ },
+ },
+ Object {
+ "and": Array [],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 4",
+ "kqlQuery": "",
+ "name": "Provider 4",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 4",
+ },
+ },
+ Object {
+ "and": Array [],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 5",
+ "kqlQuery": "",
+ "name": "Provider 5",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 5",
+ },
+ },
+ Object {
+ "and": Array [],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 6",
+ "kqlQuery": "",
+ "name": "Provider 6",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 6",
+ },
+ },
+ Object {
+ "and": Array [],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 7",
+ "kqlQuery": "",
+ "name": "Provider 7",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 7",
+ },
+ },
+ Object {
+ "and": Array [],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 8",
+ "kqlQuery": "",
+ "name": "Provider 8",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 8",
+ },
+ },
+ Object {
+ "and": Array [],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 9",
+ "kqlQuery": "",
+ "name": "Provider 9",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 9",
+ },
+ },
+ Object {
+ "and": Array [],
+ "enabled": true,
+ "excluded": false,
+ "id": "id-Provider 10",
+ "kqlQuery": "",
+ "name": "Provider 10",
+ "queryMatch": Object {
+ "field": "name",
+ "operator": ":",
+ "value": "Provider 10",
+ },
+ },
+ ]
+ }
+ end={1521862432253}
+ eventType="raw"
+ filters={Array []}
+ id="foo"
+ indexPattern={
+ Object {
+ "fields": Array [
+ Object {
+ "aggregatable": true,
+ "name": "@timestamp",
+ "searchable": true,
+ "type": "date",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "@version",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "agent.ephemeral_id",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "agent.hostname",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "agent.id",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "agent.test1",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "agent.test2",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "agent.test3",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "agent.test4",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "agent.test5",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "agent.test6",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "agent.test7",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "agent.test8",
+ "searchable": true,
+ "type": "string",
+ },
+ Object {
+ "aggregatable": true,
+ "name": "host.name",
+ "searchable": true,
+ "type": "string",
+ },
+ ],
+ "title": "filebeat-*,auditbeat-*,packetbeat-*",
+ }
+ }
+ indexToAdd={Array []}
+ isLive={false}
+ itemsPerPage={5}
+ itemsPerPageOptions={
+ Array [
+ 5,
+ 10,
+ 20,
+ ]
+ }
+ kqlMode="search"
+ kqlQueryExpression=""
+ loadingIndexName={false}
+ onChangeItemsPerPage={[MockFunction]}
+ onClose={[MockFunction]}
+ onDataProviderEdited={[MockFunction]}
+ onDataProviderRemoved={[MockFunction]}
+ onToggleDataProviderEnabled={[MockFunction]}
+ onToggleDataProviderExcluded={[MockFunction]}
+ show={true}
+ showCallOutUnauthorizedMsg={false}
+ sort={
+ Object {
+ "columnId": "@timestamp",
+ "sortDirection": "desc",
+ }
+ }
+ start={1521830963132}
+ toggleColumn={[MockFunction]}
+ usersViewing={
+ Array [
+ "elastic",
+ ]
+ }
+ />
+
+
+
+
+
+
`;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
index ef744ab562e712..b478070b315783 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
@@ -15,6 +15,7 @@ import { eventHasNotes, getPinTooltip } from '../helpers';
import * as i18n from '../translations';
import { OnRowSelected } from '../../events';
import { Ecs } from '../../../../../graphql/types';
+import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
export interface TimelineRowActionOnClick {
eventId: string;
@@ -27,7 +28,7 @@ export interface TimelineRowAction {
displayType: 'icon' | 'contextMenu';
iconType?: string;
id: string;
- isActionDisabled?: boolean;
+ isActionDisabled?: (ecsData?: Ecs) => boolean;
onClick: ({ eventId, ecsData }: TimelineRowActionOnClick) => void;
content: string | JSX.Element;
width?: number;
@@ -83,24 +84,9 @@ export const Actions = React.memo(
actionsColumnWidth={actionsColumnWidth}
data-test-subj="event-actions-container"
>
-
-
- {loading && }
-
- {!loading && (
-
- )}
-
-
{showCheckboxes && (
-
+
{loadingEventIds.includes(eventId) ? (
) : (
@@ -120,12 +106,28 @@ export const Actions = React.memo(
)}
+
+
+ {loading && }
+
+ {!loading && (
+
+ )}
+
+
+
<>{additionalActions}>
{!isEventViewer && (
<>
-
+
(
-
+
+ {showSelectAllCheckbox && (
+
+
+
+
+
+ )}
+
-
+
+
{showEventsSelect && (
-
+
)}
- {showSelectAllCheckbox && (
-
-
-
-
-
- )}
(
...acc,
icon: [
...acc.icon,
-
+
(
aria-label={action.ariaLabel}
data-test-subj={`${action.dataTestSubj}-button`}
iconType={action.iconType}
- isDisabled={action.isActionDisabled ?? false}
+ isDisabled={
+ action.isActionDisabled != null ? action.isActionDisabled(ecsData) : false
+ }
onClick={() => action.onClick({ eventId: id, ecsData })}
/>
@@ -155,7 +158,9 @@ export const EventColumnView = React.memo(
onClickCb(() => action.onClick({ eventId: id, ecsData }))}
@@ -170,7 +175,11 @@ export const EventColumnView = React.memo(
return grouped.contextMenu.length > 0
? [
...grouped.icon,
-
+
=> {
}
return 'raw';
};
+
+export const showGraphView = (graphEventId?: string) =>
+ graphEventId != null && graphEventId.length > 0;
+
+export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => {
+ return (
+ get(['agent', 'type', 0], ecsData) === 'endpoint' &&
+ get(['process', 'entity_id'], ecsData)?.length > 0
+ );
+};
+
+export const getInvestigateInResolverAction = ({
+ dispatch,
+ timelineId,
+}: {
+ dispatch: Dispatch;
+ timelineId: string;
+}): TimelineRowAction => ({
+ ariaLabel: i18n.ACTION_INVESTIGATE_IN_RESOLVER,
+ content: i18n.ACTION_INVESTIGATE_IN_RESOLVER,
+ dataTestSubj: 'investigate-in-resolver',
+ displayType: 'icon',
+ iconType: 'node',
+ id: 'investigateInResolver',
+ isActionDisabled: (ecsData?: Ecs) => !isInvestigateInResolverActionEnabled(ecsData),
+ onClick: ({ eventId }: TimelineRowActionOnClick) =>
+ dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: eventId })),
+ width: DEFAULT_ICON_BUTTON_WIDTH,
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
index 775c26e82d27bc..9b96e0c49c73d7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
@@ -70,6 +70,7 @@ describe('Body', () => {
pinnedEventIds={{}}
rowRenderers={rowRenderers}
selectedEventIds={{}}
+ show={true}
sort={mockSort}
showCheckboxes={false}
toggleColumn={jest.fn()}
@@ -108,6 +109,7 @@ describe('Body', () => {
pinnedEventIds={{}}
rowRenderers={rowRenderers}
selectedEventIds={{}}
+ show={true}
sort={mockSort}
showCheckboxes={false}
toggleColumn={jest.fn()}
@@ -146,6 +148,7 @@ describe('Body', () => {
pinnedEventIds={{}}
rowRenderers={rowRenderers}
selectedEventIds={{}}
+ show={true}
sort={mockSort}
showCheckboxes={false}
toggleColumn={jest.fn()}
@@ -186,6 +189,7 @@ describe('Body', () => {
pinnedEventIds={{}}
rowRenderers={rowRenderers}
selectedEventIds={{}}
+ show={true}
sort={mockSort}
showCheckboxes={false}
toggleColumn={jest.fn()}
@@ -271,6 +275,7 @@ describe('Body', () => {
pinnedEventIds={{}}
rowRenderers={rowRenderers}
selectedEventIds={{}}
+ show={true}
sort={mockSort}
showCheckboxes={false}
toggleColumn={jest.fn()}
@@ -316,6 +321,7 @@ describe('Body', () => {
pinnedEventIds={{}}
rowRenderers={rowRenderers}
selectedEventIds={{}}
+ show={true}
sort={mockSort}
showCheckboxes={false}
toggleColumn={jest.fn()}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
index da8835d5903e19..46895c86de084a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
@@ -26,10 +26,13 @@ import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles';
import { ColumnHeaders } from './column_headers';
import { getActionsColumnWidth } from './column_headers/helpers';
import { Events } from './events';
+import { showGraphView } from './helpers';
import { ColumnRenderer } from './renderers/column_renderer';
import { RowRenderer } from './renderers/row_renderer';
import { Sort } from './sort';
import { useManageTimeline } from '../../manage_timeline';
+import { GraphOverlay } from '../../graph_overlay';
+import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
export interface BodyProps {
addNoteToEvent: AddNoteToEvent;
@@ -38,6 +41,7 @@ export interface BodyProps {
columnRenderers: ColumnRenderer[];
data: TimelineItem[];
getNotesByIds: (noteIds: string[]) => Note[];
+ graphEventId?: string;
height?: number;
id: string;
isEventViewer?: boolean;
@@ -56,6 +60,7 @@ export interface BodyProps {
pinnedEventIds: Readonly>;
rowRenderers: RowRenderer[];
selectedEventIds: Readonly>;
+ show: boolean;
showCheckboxes: boolean;
sort: Sort;
toggleColumn: (column: ColumnHeaderOptions) => void;
@@ -72,6 +77,7 @@ export const Body = React.memo(
data,
eventIdToNoteIds,
getNotesByIds,
+ graphEventId,
height,
id,
isEventViewer = false,
@@ -89,6 +95,7 @@ export const Body = React.memo(
pinnedEventIds,
rowRenderers,
selectedEventIds,
+ show,
showCheckboxes,
sort,
toggleColumn,
@@ -108,7 +115,7 @@ export const Body = React.memo(
if (v.displayType === 'icon') {
return acc + (v.width ?? 0);
}
- const addWidth = hasContextMenu ? 0 : 26;
+ const addWidth = hasContextMenu ? 0 : DEFAULT_ICON_BUTTON_WIDTH;
hasContextMenu = true;
return acc + addWidth;
}, 0) ?? 0
@@ -127,7 +134,15 @@ export const Body = React.memo(
return (
<>
-
+ {showGraphView(graphEventId) && (
+
+ )}
+
(
selectedEventIds,
setSelected,
clearSelected,
+ show,
showCheckboxes,
showRowRenderers,
+ graphEventId,
sort,
toggleColumn,
unPinEvent,
@@ -180,6 +183,7 @@ const StatefulBodyComponent = React.memo(
data={data}
eventIdToNoteIds={eventIdToNoteIds}
getNotesByIds={getNotesByIds}
+ graphEventId={graphEventId}
height={height}
id={id}
isEventViewer={isEventViewer}
@@ -197,6 +201,7 @@ const StatefulBodyComponent = React.memo(
pinnedEventIds={pinnedEventIds}
rowRenderers={showRowRenderers ? rowRenderers : [plainRowRenderer]}
selectedEventIds={selectedEventIds}
+ show={id === ACTIVE_TIMELINE_REDUX_ID ? show : true}
showCheckboxes={showCheckboxes}
sort={sort}
toggleColumn={toggleColumn}
@@ -209,6 +214,7 @@ const StatefulBodyComponent = React.memo(
deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) &&
deepEqual(prevProps.data, nextProps.data) &&
prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds &&
+ prevProps.graphEventId === nextProps.graphEventId &&
deepEqual(prevProps.notesById, nextProps.notesById) &&
prevProps.height === nextProps.height &&
prevProps.id === nextProps.id &&
@@ -216,6 +222,7 @@ const StatefulBodyComponent = React.memo(
prevProps.isSelectAllChecked === nextProps.isSelectAllChecked &&
prevProps.loadingEventIds === nextProps.loadingEventIds &&
prevProps.pinnedEventIds === nextProps.pinnedEventIds &&
+ prevProps.show === nextProps.show &&
prevProps.selectedEventIds === nextProps.selectedEventIds &&
prevProps.showCheckboxes === nextProps.showCheckboxes &&
prevProps.showRowRenderers === nextProps.showRowRenderers &&
@@ -238,10 +245,12 @@ const makeMapStateToProps = () => {
columns,
eventIdToNoteIds,
eventType,
+ graphEventId,
isSelectAllChecked,
loadingEventIds,
pinnedEventIds,
selectedEventIds,
+ show,
showCheckboxes,
showRowRenderers,
} = timeline;
@@ -250,12 +259,14 @@ const makeMapStateToProps = () => {
columnHeaders: memoizedColumnHeaders(columns, browserFields),
eventIdToNoteIds,
eventType,
+ graphEventId,
isSelectAllChecked,
loadingEventIds,
notesById: getNotesByIds(state),
id,
pinnedEventIds,
selectedEventIds,
+ show,
showCheckboxes,
showRowRenderers,
};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
index 98f544f30ae8b5..63b92d6b316cc8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
@@ -51,3 +51,10 @@ export const COLLAPSE = i18n.translate(
defaultMessage: 'Collapse',
}
);
+
+export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate(
+ 'xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip',
+ {
+ defaultMessage: 'Investigate in Resolver',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
index fb47eb331fdbbf..e8f1e73719234d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
@@ -9,6 +9,7 @@ import React from 'react';
import { FilterManager, IIndexPattern } from 'src/plugins/data/public';
import deepEqual from 'fast-deep-equal';
+import { showGraphView } from '../body/helpers';
import { DataProviders } from '../data_providers';
import { DataProvider } from '../data_providers/data_provider';
import {
@@ -26,6 +27,7 @@ interface Props {
browserFields: BrowserFields;
dataProviders: DataProvider[];
filterManager: FilterManager;
+ graphEventId?: string;
id: string;
indexPattern: IIndexPattern;
onDataProviderEdited: OnDataProviderEdited;
@@ -42,6 +44,7 @@ const TimelineHeaderComponent: React.FC = ({
indexPattern,
dataProviders,
filterManager,
+ graphEventId,
onDataProviderEdited,
onDataProviderRemoved,
onToggleDataProviderEnabled,
@@ -59,24 +62,27 @@ const TimelineHeaderComponent: React.FC = ({
size="s"
/>
)}
- {show && (
-
- )}
-
+ {show && !showGraphView(graphEventId) && (
+ <>
+
+
+
+ >
+ )}
>
);
@@ -88,6 +94,7 @@ export const TimelineHeader = React.memo(
deepEqual(prevProps.indexPattern, nextProps.indexPattern) &&
deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
prevProps.filterManager === nextProps.filterManager &&
+ prevProps.graphEventId === nextProps.graphEventId &&
prevProps.onDataProviderEdited === nextProps.onDataProviderEdited &&
prevProps.onDataProviderRemoved === nextProps.onDataProviderRemoved &&
prevProps.onToggleDataProviderEnabled === nextProps.onToggleDataProviderEnabled &&
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
index b5481e9d4eee24..a3fc692c3a8a85 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
@@ -153,3 +153,5 @@ export const combineQueries = ({
* the `Timeline` and the `Events Viewer` widget
*/
export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view';
+
+export const DEFAULT_ICON_BUTTON_WIDTH = 24;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
index 5ccc8911d1974b..83ac1a421958be 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
@@ -72,6 +72,7 @@ describe('StatefulTimeline', () => {
eventType: 'raw',
end: endDate,
filters: [],
+ graphEventId: undefined,
id: 'foo',
isLive: false,
isTimelineExists: false,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
index df76eb350ace7f..a66c01d0b5d0b9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
@@ -41,6 +41,7 @@ const StatefulTimelineComponent = React.memo(
eventType,
end,
filters,
+ graphEventId,
id,
isLive,
isTimelineExists,
@@ -168,6 +169,7 @@ const StatefulTimelineComponent = React.memo(
end={end}
eventType={eventType}
filters={filters}
+ graphEventId={graphEventId}
id={id}
indexPattern={indexPattern}
indexToAdd={indexToAdd}
@@ -196,6 +198,7 @@ const StatefulTimelineComponent = React.memo(
return (
prevProps.eventType === nextProps.eventType &&
prevProps.end === nextProps.end &&
+ prevProps.graphEventId === nextProps.graphEventId &&
prevProps.id === nextProps.id &&
prevProps.isLive === nextProps.isLive &&
prevProps.itemsPerPage === nextProps.itemsPerPage &&
@@ -229,6 +232,7 @@ const makeMapStateToProps = () => {
dataProviders,
eventType,
filters,
+ graphEventId,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
@@ -245,6 +249,7 @@ const makeMapStateToProps = () => {
eventType,
end: input.timerange.to,
filters: timelineFilter,
+ graphEventId,
id,
isLive: input.policy.kind === 'interval',
isTimelineExists: getTimeline(state, id) != null,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx
index 2ffbae1f7eb5c0..5e6f35e8397e48 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx
@@ -50,7 +50,11 @@ describe('Insert timeline popover ', () => {
payload: { id: 'timeline-id', show: false },
type: 'x-pack/security_solution/local/timeline/SHOW_TIMELINE',
});
- expect(onTimelineChange).toBeCalledWith('Timeline title', '34578-3497-5893-47589-34759');
+ expect(onTimelineChange).toBeCalledWith(
+ 'Timeline title',
+ '34578-3497-5893-47589-34759',
+ undefined
+ );
expect(mockDispatch.mock.calls[1][0]).toEqual({
payload: null,
type: 'x-pack/security_solution/local/timeline/SET_INSERT_TIMELINE',
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx
index de199d9a1cc2eb..83417cdb51b699 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx
@@ -19,7 +19,11 @@ import { setInsertTimeline } from '../../../store/timeline/actions';
interface InsertTimelinePopoverProps {
isDisabled: boolean;
hideUntitled?: boolean;
- onTimelineChange: (timelineTitle: string, timelineId: string | null) => void;
+ onTimelineChange: (
+ timelineTitle: string,
+ timelineId: string | null,
+ graphEventId?: string
+ ) => void;
}
type Props = InsertTimelinePopoverProps;
@@ -38,7 +42,11 @@ export const InsertTimelinePopoverComponent: React.FC = ({
useEffect(() => {
if (insertTimeline != null) {
dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false }));
- onTimelineChange(insertTimeline.timelineTitle, insertTimeline.timelineSavedObjectId);
+ onTimelineChange(
+ insertTimeline.timelineTitle,
+ insertTimeline.timelineSavedObjectId,
+ insertTimeline.graphEventId
+ );
dispatch(setInsertTimeline(null));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx
index c3def9c4cbb292..c3bcd1c0ebe516 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { isEmpty } from 'lodash/fp';
import { useCallback, useState } from 'react';
import { useBasePath } from '../../../../common/lib/kibana';
import { CursorPosition } from '../../../../common/components/markdown_editor';
@@ -16,8 +17,10 @@ export const useInsertTimeline = (form: FormHook, fieldNa
end: 0,
});
const handleOnTimelineChange = useCallback(
- (title: string, id: string | null) => {
- const builtLink = `${basePath}/app/security/timelines?timeline=(id:'${id}',isOpen:!t)`;
+ (title: string, id: string | null, graphEventId?: string) => {
+ const builtLink = `${basePath}/app/security/timelines?timeline=(id:'${id}'${
+ !isEmpty(graphEventId) ? `,graphEventId:'${graphEventId}'` : ''
+ },isOpen:!t)`;
const currentValue = form.getFormData()[fieldName];
const newValue: string = [
currentValue.slice(0, cursorPosition.start),
@@ -28,16 +31,12 @@ export const useInsertTimeline = (form: FormHook, fieldNa
].join('');
form.setFieldValue(fieldName, newValue);
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [form]
- );
- const handleCursorChange = useCallback(
- (cp: CursorPosition) => {
- setCursorPosition(cp);
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [cursorPosition]
+ [basePath, cursorPosition, fieldName, form]
);
+ const handleCursorChange = useCallback((cp: CursorPosition) => {
+ setCursorPosition(cp);
+ }, []);
+
return {
cursorPosition,
handleCursorChange,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx
index d8c9d2ed02cc6e..aec09a95b4b195 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx
@@ -17,6 +17,7 @@ jest.mock('../../../../common/lib/kibana', () => {
useKibana: jest.fn().mockReturnValue({
services: {
application: {
+ navigateToApp: jest.fn(),
capabilities: {
siem: {
crud: true,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
index f2e7d26c9e8516..528af23191ee9b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
@@ -20,7 +20,6 @@ import {
import React, { useCallback } from 'react';
import uuid from 'uuid';
import styled from 'styled-components';
-import { useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { APP_ID } from '../../../../../common/constants';
@@ -28,11 +27,10 @@ import {
TimelineTypeLiteral,
TimelineStatus,
TimelineType,
+ TimelineId,
} from '../../../../../common/types/timeline';
-import { navTabs } from '../../../../app/home/home_navigations';
import { SecurityPageName } from '../../../../app/types';
import { timelineSelectors } from '../../../../timelines/store/timeline';
-import { useGetUrlSearch } from '../../../../common/components/navigation/use_get_url_search';
import { getCreateCaseUrl } from '../../../../common/components/link_to';
import { State } from '../../../../common/store';
import { useKibana } from '../../../../common/lib/kibana';
@@ -44,7 +42,7 @@ import { AssociateNote, UpdateNote } from '../../notes/helpers';
import { NOTES_PANEL_WIDTH } from './notes_size';
import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles';
import * as i18n from './translations';
-import { setInsertTimeline } from '../../../store/timeline/actions';
+import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions';
import { useCreateTimelineButton } from './use_create_timeline';
export const historyToolTip = 'The chronological history of actions related to this timeline';
@@ -139,6 +137,8 @@ export const Name = React.memo(({ timelineId, title, updateTitle }) =
Name.displayName = 'Name';
interface NewCaseProps {
+ compact?: boolean;
+ graphEventId?: string;
onClosePopover: () => void;
timelineId: string;
timelineStatus: TimelineStatus;
@@ -146,44 +146,50 @@ interface NewCaseProps {
}
export const NewCase = React.memo(
- ({ onClosePopover, timelineId, timelineStatus, timelineTitle }) => {
- const history = useHistory();
- const urlSearch = useGetUrlSearch(navTabs.case);
+ ({ compact, graphEventId, onClosePopover, timelineId, timelineStatus, timelineTitle }) => {
const dispatch = useDispatch();
const { savedObjectId } = useSelector((state: State) =>
timelineSelectors.selectTimeline(state, timelineId)
);
const { navigateToApp } = useKibana().services.application;
+ const buttonText = compact ? i18n.ATTACH_TO_NEW_CASE : i18n.ATTACH_TIMELINE_TO_NEW_CASE;
const handleClick = useCallback(() => {
onClosePopover();
dispatch(
setInsertTimeline({
+ graphEventId,
timelineId,
timelineSavedObjectId: savedObjectId,
timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE,
})
);
+ dispatch(showTimeline({ id: TimelineId.active, show: false }));
+
navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
- path: getCreateCaseUrl(urlSearch),
- });
- history.push({
- pathname: `/${SecurityPageName.case}/create`,
+ path: getCreateCaseUrl(),
});
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [dispatch, navigateToApp, onClosePopover, history, timelineId, timelineTitle, urlSearch]);
+ }, [
+ dispatch,
+ graphEventId,
+ navigateToApp,
+ onClosePopover,
+ savedObjectId,
+ timelineId,
+ timelineTitle,
+ ]);
return (
- {i18n.ATTACH_TIMELINE_TO_NEW_CASE}
+ {buttonText}
);
}
@@ -191,28 +197,33 @@ export const NewCase = React.memo(
NewCase.displayName = 'NewCase';
interface ExistingCaseProps {
+ compact?: boolean;
onClosePopover: () => void;
onOpenCaseModal: () => void;
timelineStatus: TimelineStatus;
}
export const ExistingCase = React.memo(
- ({ onClosePopover, onOpenCaseModal, timelineStatus }) => {
+ ({ compact, onClosePopover, onOpenCaseModal, timelineStatus }) => {
const handleClick = useCallback(() => {
onClosePopover();
onOpenCaseModal();
}, [onOpenCaseModal, onClosePopover]);
+ const buttonText = compact
+ ? i18n.ATTACH_TO_EXISTING_CASE
+ : i18n.ATTACH_TIMELINE_TO_EXISTING_CASE;
return (
<>
- {i18n.ATTACH_TIMELINE_TO_EXISTING_CASE}
+ {buttonText}
>
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
index 3078700a29d76b..1b76db409484f4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
@@ -17,7 +17,6 @@ import {
import { createStore, State } from '../../../../common/store';
import { useThrottledResizeObserver } from '../../../../common/components/utils';
import { Properties, showDescriptionThreshold, showNotesThreshold } from '.';
-import { SecurityPageName } from '../../../../app/types';
import { setInsertTimeline } from '../../../store/timeline/actions';
export { nextTick } from '../../../../../../../test_utils';
@@ -25,12 +24,13 @@ import { act } from 'react-dom/test-utils';
jest.mock('../../../../common/components/link_to');
+const mockNavigateToApp = jest.fn();
jest.mock('../../../../common/lib/kibana', () => {
const original = jest.requireActual('../../../../common/lib/kibana');
return {
...original,
- useKibana: jest.fn().mockReturnValue({
+ useKibana: () => ({
services: {
application: {
capabilities: {
@@ -38,7 +38,7 @@ jest.mock('../../../../common/lib/kibana', () => {
crud: true,
},
},
- navigateToApp: jest.fn(),
+ navigateToApp: mockNavigateToApp,
},
},
}),
@@ -63,7 +63,6 @@ jest.mock('react-redux', () => {
useSelector: jest.fn().mockReturnValue({ savedObjectId: '1', urlState: {} }),
};
});
-const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@@ -71,7 +70,7 @@ jest.mock('react-router-dom', () => {
return {
...original,
useHistory: () => ({
- push: mockHistoryPush,
+ push: jest.fn(),
}),
};
});
@@ -342,8 +341,7 @@ describe('Properties', () => {
);
wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click');
wrapper.find('[data-test-subj="attach-timeline-case"]').first().simulate('click');
-
- expect(mockHistoryPush).toBeCalledWith({ pathname: `/${SecurityPageName.case}/create` });
+ expect(mockNavigateToApp).toBeCalledWith('securitySolution:case', { path: '/create' });
expect(mockDispatch).toBeCalledWith(
setInsertTimeline({
timelineId: defaultProps.timelineId,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
index 602a7c8191c7a2..8029d166a688a5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
@@ -46,6 +46,7 @@ interface Props {
createTimeline: CreateTimeline;
description: string;
getNotesByIds: (noteIds: string[]) => Note[];
+ graphEventId?: string;
isDataInTimeline: boolean;
isDatepickerLocked: boolean;
isFavorite: boolean;
@@ -79,6 +80,7 @@ export const Properties = React.memo(
createTimeline,
description,
getNotesByIds,
+ graphEventId,
isDataInTimeline,
isDatepickerLocked,
isFavorite,
@@ -120,18 +122,21 @@ export const Properties = React.memo(
const onRowClick = useCallback(
(id: string) => {
onCloseCaseModal();
- navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
- path: getCaseDetailsUrl({ id }),
- });
+
dispatch(
setInsertTimeline({
+ graphEventId,
timelineId,
timelineSavedObjectId: currentTimeline.savedObjectId,
timelineTitle: title.length > 0 ? title : i18n.UNTITLED_TIMELINE,
})
);
+
+ navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
+ path: getCaseDetailsUrl({ id }),
+ });
},
- [navigateToApp, onCloseCaseModal, currentTimeline, dispatch, timelineId, title]
+ [currentTimeline, dispatch, graphEventId, navigateToApp, onCloseCaseModal, timelineId, title]
);
const datePickerWidth = useMemo(
@@ -174,6 +179,7 @@ export const Properties = React.memo(
associateNote={associateNote}
description={description}
getNotesByIds={getNotesByIds}
+ graphEventId={graphEventId}
isDataInTimeline={isDataInTimeline}
noteIds={noteIds}
onButtonClick={onButtonClick}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
index 7d176d57b5d818..e20a3db80d8812 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
@@ -68,6 +68,7 @@ interface PropertiesRightComponentProps {
associateNote: AssociateNote;
description: string;
getNotesByIds: (noteIds: string[]) => Note[];
+ graphEventId?: string;
isDataInTimeline: boolean;
noteIds: string[];
onButtonClick: () => void;
@@ -94,6 +95,7 @@ const PropertiesRightComponent: React.FC = ({
associateNote,
description,
getNotesByIds,
+ graphEventId,
isDataInTimeline,
noteIds,
onButtonClick,
@@ -166,6 +168,7 @@ const PropertiesRightComponent: React.FC = ({
EuiSelectableOption[];
onClosePopover: () => void;
- onTimelineChange: (timelineTitle: string, timelineId: string | null) => void;
+ onTimelineChange: (
+ timelineTitle: string,
+ timelineId: string | null,
+ graphEventId?: string
+ ) => void;
timelineType: TimelineTypeLiteral;
}
@@ -202,7 +206,8 @@ const SelectableTimelineComponent: React.FC = ({
isEmpty(selectedTimeline[0].title)
? i18nTimeline.UNTITLED_TIMELINE
: selectedTimeline[0].title,
- selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id
+ selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id,
+ selectedTimeline[0].graphEventId ?? ''
);
}
onClosePopover();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
index aad80cbdfe3372..55bcbbecda2690 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
@@ -24,11 +24,12 @@ export const TimelineBodyGlobalStyle = createGlobalStyle`
export const TimelineBody = styled.div.attrs(({ className = '' }) => ({
className: `siemTimeline__body ${className}`,
-}))<{ bodyHeight?: number }>`
+}))<{ bodyHeight?: number; visible: boolean }>`
height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')};
overflow: auto;
scrollbar-width: thin;
flex: 1;
+ visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
&::-webkit-scrollbar {
height: ${({ theme }) => theme.eui.euiScrollBar};
@@ -89,10 +90,9 @@ export const EventsTrHeader = styled.div.attrs(({ className }) => ({
export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsTable__thGroupActions ${className}`,
-}))<{ actionsColumnWidth: number; justifyContent: string }>`
+}))<{ actionsColumnWidth: number }>`
display: flex;
flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`};
- justify-content: ${({ justifyContent }) => justifyContent};
min-width: 0;
`;
@@ -139,14 +139,17 @@ export const EventsTh = styled.div.attrs(({ className = '' }) => ({
export const EventsThContent = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsTable__thContent ${className}`,
-}))<{ textAlign?: string }>`
+}))<{ textAlign?: string; width?: number }>`
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
line-height: ${({ theme }) => theme.eui.euiLineHeight};
min-width: 0;
padding: ${({ theme }) => theme.eui.paddingSizes.xs};
text-align: ${({ textAlign }) => textAlign};
- width: 100%; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
+ width: ${({ width }) =>
+ width != null
+ ? `${width}px`
+ : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
`;
/* EVENTS BODY */
@@ -202,7 +205,6 @@ export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsTable__tdGroupActions ${className}`,
}))<{ actionsColumnWidth: number }>`
display: flex;
- justify-content: space-between;
flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`};
min-width: 0;
`;
@@ -234,14 +236,17 @@ export const EventsTd = styled.div.attrs(({ className = '', width })
`;
export const EventsTdContent = styled.div.attrs(({ className }) => ({
- className: `siemEventsTable__tdContent ${className}`,
-}))<{ textAlign?: string }>`
+ className: `siemEventsTable__tdContent ${className != null ? className : ''}`,
+}))<{ textAlign?: string; width?: number }>`
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
line-height: ${({ theme }) => theme.eui.euiLineHeight};
min-width: 0;
padding: ${({ theme }) => theme.eui.paddingSizes.xs};
text-align: ${({ textAlign }) => textAlign};
- width: 100%; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
+ width: ${({ width }) =>
+ width != null
+ ? `${width}px`
+ : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
`;
/**
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
index 96703941f616e3..79ec58711e06c4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
@@ -103,7 +103,11 @@ describe('Timeline', () => {
describe('rendering', () => {
test('renders correctly against snapshot', () => {
- const wrapper = shallow();
+ const wrapper = shallow(
+
+
+
+ );
expect(wrapper).toMatchSnapshot();
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
index 884d693ca6ade0..85e3d5d9478b63 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
@@ -7,6 +7,7 @@
import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui';
import { getOr, isEmpty } from 'lodash/fp';
import React, { useState, useMemo, useEffect } from 'react';
+import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button';
@@ -16,6 +17,7 @@ import { Direction } from '../../../graphql/types';
import { useKibana } from '../../../common/lib/kibana';
import { ColumnHeaderOptions, KqlMode, EventType } from '../../../timelines/store/timeline/model';
import { defaultHeaders } from './body/column_headers/default_headers';
+import { getInvestigateInResolverAction } from './body/helpers';
import { Sort } from './body/sort';
import { StatefulBody } from './body/stateful_body';
import { DataProvider } from './data_providers/data_provider';
@@ -88,6 +90,7 @@ export interface Props {
end: number;
eventType?: EventType;
filters: Filter[];
+ graphEventId?: string;
id: string;
indexPattern: IIndexPattern;
indexToAdd: string[];
@@ -119,6 +122,7 @@ export const TimelineComponent: React.FC = ({
end,
eventType,
filters,
+ graphEventId,
id,
indexPattern,
indexToAdd,
@@ -141,6 +145,7 @@ export const TimelineComponent: React.FC = ({
toggleColumn,
usersViewing,
}) => {
+ const dispatch = useDispatch();
const kibana = useKibana();
const [filterManager] = useState(new FilterManager(kibana.services.uiSettings));
const combinedQueries = combineQueries({
@@ -168,9 +173,14 @@ export const TimelineComponent: React.FC = ({
initializeTimeline,
setIsTimelineLoading,
setTimelineFilterManager,
+ setTimelineRowActions,
} = useManageTimeline();
useEffect(() => {
initializeTimeline({ id, indexToAdd });
+ setTimelineRowActions({
+ id,
+ timelineRowActions: [getInvestigateInResolverAction({ dispatch, timelineId: id })],
+ });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
@@ -197,6 +207,7 @@ export const TimelineComponent: React.FC = ({
indexPattern={indexPattern}
dataProviders={dataProviders}
filterManager={filterManager}
+ graphEventId={graphEventId}
onDataProviderEdited={onDataProviderEdited}
onDataProviderRemoved={onDataProviderRemoved}
onToggleDataProviderEnabled={onToggleDataProviderEnabled}
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts
index 53d0b98570bcbf..e2a268e750b4a5 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts
+++ b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts
@@ -89,6 +89,9 @@ export const timelineQuery = gql`
timezone
type
}
+ agent {
+ type
+ }
auditd {
result
session
@@ -285,6 +288,7 @@ export const timelineQuery = gql`
name
ppid
args
+ entity_id
executable
title
working_directory
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
index c5df017604b0c6..55e6849fdb6c4a 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
@@ -87,6 +87,10 @@ export const removeProvider = actionCreator<{
export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE');
+export const updateTimelineGraphEventId = actionCreator<{ id: string; graphEventId: string }>(
+ 'UPDATE_TIMELINE_GRAPH_EVENT_ID'
+);
+
export const unPinEvent = actionCreator<{ id: string; eventId: string }>('UN_PIN_EVENT');
export const updateTimeline = actionCreator<{
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
index 15f956fa79d3cc..c0615d36f7a2e0 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
@@ -228,6 +228,26 @@ export const updateTimelineShowTimeline = ({
};
};
+export const updateGraphEventId = ({
+ id,
+ graphEventId,
+ timelineById,
+}: {
+ id: string;
+ graphEventId: string;
+ timelineById: TimelineById;
+}): TimelineById => {
+ const timeline = timelineById[id];
+
+ return {
+ ...timelineById,
+ [id]: {
+ ...timeline,
+ graphEventId,
+ },
+ };
+};
+
interface ApplyDeltaToCurrentWidthParams {
id: string;
delta: number;
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
index caad70226365af..e8ea3c8d16e3a8 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
@@ -55,6 +55,8 @@ export interface TimelineModel {
/** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */
eventIdToNoteIds: Record;
filters?: Filter[];
+ /** When non-empty, display a graph view for this event */
+ graphEventId?: string;
/** The chronological history of actions related to this timeline */
historyIds: string[];
/** The chronological history of actions related to this timeline */
@@ -129,6 +131,7 @@ export type SubsetTimelineModel = Readonly<
| 'description'
| 'eventType'
| 'eventIdToNoteIds'
+ | 'graphEventId'
| 'highlightedDropAndProviderId'
| 'historyIds'
| 'isFavorite'
@@ -165,4 +168,5 @@ export type SubsetTimelineModel = Readonly<
export interface TimelineUrl {
id: string;
isOpen: boolean;
+ graphEventId?: string;
}
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
index 3bdb16be79939a..6e7a36079a0c34 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
@@ -1788,6 +1788,7 @@ describe('Timeline', () => {
isLoading: false,
id: 'foo',
savedObjectId: null,
+ showRowRenderers: true,
kqlMode: 'filter',
kqlQuery: { filterQuery: null, filterQueryDraft: null },
loadingEventIds: [],
@@ -1802,7 +1803,6 @@ describe('Timeline', () => {
},
selectedEventIds: {},
show: true,
- showRowRenderers: true,
showCheckboxes: false,
sort: {
columnId: '@timestamp',
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
index 5e314f15974513..30b7f73c839d19 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
@@ -53,6 +53,7 @@ import {
updateRange,
updateSort,
updateTimeline,
+ updateTimelineGraphEventId,
updateTitle,
upsertColumn,
} from './actions';
@@ -94,6 +95,7 @@ import {
updateTimelineTitle,
upsertTimelineColumn,
updateSavedQuery,
+ updateGraphEventId,
updateFilters,
updateTimelineEventType,
} from './helpers';
@@ -194,6 +196,10 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
...state,
timelineById: updateTimelineShowTimeline({ id, show, timelineById: state.timelineById }),
}))
+ .case(updateTimelineGraphEventId, (state, { id, graphEventId }) => ({
+ ...state,
+ timelineById: updateGraphEventId({ id, graphEventId, timelineById: state.timelineById }),
+ }))
.case(applyDeltaToColumnWidth, (state, { id, columnId, delta }) => ({
...state,
timelineById: applyDeltaToTimelineColumnWidth({
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts
index 5262c72a6140c9..65798648f92c63 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts
@@ -23,6 +23,7 @@ export interface TimelineById {
}
export interface InsertTimeline {
+ graphEventId?: string;
timelineId: string;
timelineSavedObjectId: string | null;
timelineTitle: string;
diff --git a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts
index 9bf55cfe1ed2a0..52011e14167173 100644
--- a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts
+++ b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts
@@ -60,6 +60,10 @@ export const ecsSchema = gql`
sequence: ToStringArray
}
+ type AgentEcsField {
+ type: ToStringArray
+ }
+
type AuditdData {
acct: ToStringArray
terminal: ToStringArray
@@ -110,6 +114,7 @@ export const ecsSchema = gql`
name: ToStringArray
ppid: ToNumberArray
args: ToStringArray
+ entity_id: ToStringArray
executable: ToStringArray
title: ToStringArray
thread: Thread
@@ -425,6 +430,7 @@ export const ecsSchema = gql`
type ECS {
_id: String!
_index: String
+ agent: AgentEcsField
auditd: AuditdEcsFields
destination: DestinationEcsFields
dns: DnsEcsFields
diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts
index 4a063647a183d9..40666b61939280 100644
--- a/x-pack/plugins/security_solution/server/graphql/types.ts
+++ b/x-pack/plugins/security_solution/server/graphql/types.ts
@@ -765,6 +765,8 @@ export interface Ecs {
_index?: Maybe;
+ agent?: Maybe;
+
auditd?: Maybe;
destination?: Maybe;
@@ -812,6 +814,10 @@ export interface Ecs {
system?: Maybe;
}
+export interface AgentEcsField {
+ type?: Maybe;
+}
+
export interface AuditdEcsFields {
result?: Maybe;
@@ -1267,6 +1273,8 @@ export interface ProcessEcsFields {
args?: Maybe;
+ entity_id?: Maybe;
+
executable?: Maybe;
title?: Maybe;
@@ -4083,6 +4091,8 @@ export namespace EcsResolvers {
_index?: _IndexResolver, TypeParent, TContext>;
+ agent?: AgentResolver, TypeParent, TContext>;
+
auditd?: AuditdResolver, TypeParent, TContext>;
destination?: DestinationResolver, TypeParent, TContext>;
@@ -4140,6 +4150,11 @@ export namespace EcsResolvers {
Parent,
TContext
>;
+ export type AgentResolver<
+ R = Maybe,
+ Parent = Ecs,
+ TContext = SiemContext
+ > = Resolver;
export type AuditdResolver<
R = Maybe,
Parent = Ecs,
@@ -4257,6 +4272,18 @@ export namespace EcsResolvers {
> = Resolver;
}
+export namespace AgentEcsFieldResolvers {
+ export interface Resolvers {
+ type?: TypeResolver, TypeParent, TContext>;
+ }
+
+ export type TypeResolver<
+ R = Maybe,
+ Parent = AgentEcsField,
+ TContext = SiemContext
+ > = Resolver;
+}
+
export namespace AuditdEcsFieldsResolvers {
export interface Resolvers {
result?: ResultResolver, TypeParent, TContext>;
@@ -5761,6 +5788,8 @@ export namespace ProcessEcsFieldsResolvers {
args?: ArgsResolver, TypeParent, TContext>;
+ entity_id?: EntityIdResolver, TypeParent, TContext>;
+
executable?: ExecutableResolver, TypeParent, TContext>;
title?: TitleResolver, TypeParent, TContext>;
@@ -5795,6 +5824,11 @@ export namespace ProcessEcsFieldsResolvers {
Parent = ProcessEcsFields,
TContext = SiemContext
> = Resolver;
+ export type EntityIdResolver<
+ R = Maybe,
+ Parent = ProcessEcsFields,
+ TContext = SiemContext
+ > = Resolver;
export type ExecutableResolver<
R = Maybe,
Parent = ProcessEcsFields,
@@ -9110,6 +9144,7 @@ export type IResolvers = {
TimelineItem?: TimelineItemResolvers.Resolvers;
TimelineNonEcsData?: TimelineNonEcsDataResolvers.Resolvers;
Ecs?: EcsResolvers.Resolvers;
+ AgentEcsField?: AgentEcsFieldResolvers.Resolvers;
AuditdEcsFields?: AuditdEcsFieldsResolvers.Resolvers;
AuditdData?: AuditdDataResolvers.Resolvers;
Summary?: SummaryResolvers.Resolvers;
diff --git a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts
index f2662c79d33937..ff474c4a841f62 100644
--- a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts
+++ b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts
@@ -76,12 +76,17 @@ export const processFieldsMap: Readonly> = {
'process.name': 'process.name',
'process.ppid': 'process.ppid',
'process.args': 'process.args',
+ 'process.entity_id': 'process.entity_id',
'process.executable': 'process.executable',
'process.title': 'process.title',
'process.thread': 'process.thread',
'process.working_directory': 'process.working_directory',
};
+export const agentFieldsMap: Readonly> = {
+ 'agent.type': 'agent.type',
+};
+
export const userFieldsMap: Readonly> = {
'user.domain': 'user.domain',
'user.id': 'user.id',
@@ -327,6 +332,7 @@ export const eventFieldsMap: Readonly> = {
timestamp: '@timestamp',
'@timestamp': '@timestamp',
message: 'message',
+ ...{ ...agentFieldsMap },
...{ ...auditdMap },
...{ ...destinationFieldsMap },
...{ ...dnsFieldsMap },