From 8d117ca3492c8036982857baebc3bdda1e9f3d2a Mon Sep 17 00:00:00 2001 From: Paulo Henrique Date: Mon, 28 Mar 2022 20:49:15 -0300 Subject: [PATCH] [Security solution][Session view] - Add Sessions tab into the Hosts page (#127920) * add Session Leader Table * WIP: Session Leader Table * sessions search strategy * session viewer component * add timelineId * remove session leader table * cleaning * cleaning * updating search strategy * add space for open in session viewer icon * add sessionEntityId as key cache * updating deep links * updating headers * adding filterQuery * adding timeline * add runtime fields to search strategy * updating comment * fixing tests * removing unecessary intermediate component * removing intermediary component * adding tests for session viewer * remove unnecessary runtime_mappings Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../security_solution/common/constants.ts | 1 + .../common/types/timeline/index.ts | 2 + x-pack/plugins/security_solution/kibana.json | 3 +- .../public/app/deep_links/index.ts | 7 + .../__snapshots__/index.test.tsx.snap | 107 ++++++++++++++ .../sessions_viewer/default_headers.ts | 63 ++++++++ .../components/sessions_viewer/index.test.tsx | 137 ++++++++++++++++++ .../components/sessions_viewer/index.tsx | 111 ++++++++++++++ .../sessions_viewer/translations.ts | 22 +++ .../components/sessions_viewer/types.ts | 18 +++ .../common/components/top_n/helpers.test.tsx | 1 + .../public/common/mock/global_state.ts | 2 + .../security_solution/public/hosts/index.ts | 1 + .../hosts/pages/details/details_tabs.tsx | 4 + .../public/hosts/pages/details/nav_tabs.tsx | 6 + .../public/hosts/pages/details/utils.ts | 1 + .../public/hosts/pages/hosts_tabs.tsx | 4 + .../public/hosts/pages/index.tsx | 6 +- .../public/hosts/pages/nav_tabs.tsx | 6 + .../public/hosts/pages/navigation/index.ts | 1 + .../pages/navigation/sessions_tab_body.tsx | 35 +++++ .../public/hosts/pages/translations.ts | 7 + .../public/hosts/store/helpers.test.ts | 16 ++ .../public/hosts/store/model.ts | 2 + .../public/hosts/store/reducer.ts | 8 + .../plugins/security_solution/public/types.ts | 3 +- .../plugins/security_solution/tsconfig.json | 3 +- x-pack/plugins/session_view/public/index.ts | 2 + x-pack/plugins/session_view/public/types.ts | 11 +- .../search_strategy/timeline/events/index.ts | 1 + .../timelines/common/types/timeline/index.ts | 2 + .../timelines/public/store/t_grid/types.ts | 1 + .../timeline/factory/helpers/constants.ts | 5 + .../server/search_strategy/timeline/index.ts | 55 +++++++ 34 files changed, 644 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/sessions_viewer/types.ts create mode 100644 x-pack/plugins/security_solution/public/hosts/pages/navigation/sessions_tab_body.tsx diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index f205c6c439dc9a..0d0802315063f0 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -118,6 +118,7 @@ export enum SecurityPageName { users = 'users', usersAnomalies = 'users-anomalies', usersRisk = 'users-risk', + sessions = 'sessions', } export const TIMELINES_PATH = '/timelines' as const; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 5e933efbbc61d6..93dd6f9efb671c 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -318,6 +318,7 @@ export enum TimelineId { usersPageExternalAlerts = 'users-page-external-alerts', hostsPageEvents = 'hosts-page-events', hostsPageExternalAlerts = 'hosts-page-external-alerts', + hostsPageSessions = 'hosts-page-sessions', detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', @@ -332,6 +333,7 @@ export const TimelineIdLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineId.usersPageExternalAlerts), runtimeTypes.literal(TimelineId.hostsPageEvents), runtimeTypes.literal(TimelineId.hostsPageExternalAlerts), + runtimeTypes.literal(TimelineId.hostsPageSessions), runtimeTypes.literal(TimelineId.detectionsRulesDetailsPage), runtimeTypes.literal(TimelineId.detectionsPage), runtimeTypes.literal(TimelineId.networkPageExternalAlerts), diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 36edfd43d5ea5d..bd18b5d4acc319 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -25,7 +25,8 @@ "taskManager", "timelines", "triggersActionsUi", - "uiActions" + "uiActions", + "sessionView" ], "optionalPlugins": [ "encryptedSavedObjects", diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index efb220467c9d06..2df243d9b2d419 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -221,6 +221,13 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ path: `${HOSTS_PATH}/anomalies`, isPremium: true, }, + { + id: SecurityPageName.sessions, + title: i18n.translate('xpack.securitySolution.search.hosts.sessions', { + defaultMessage: 'Sessions', + }), + path: `${HOSTS_PATH}/sessions`, + }, ], }, { diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000000..32268e2f21e7fb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SessionsView renders correctly against snapshot 1`] = ` + + .c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c1 > * { + max-width: 100%; +} + +.c1 .inspectButtonComponent { + pointer-events: none; + opacity: 0; + -webkit-transition: opacity 250ms ease; + transition: opacity 250ms ease; +} + +.c1:hover .inspectButtonComponent { + pointer-events: auto; + opacity: 1; +} + +.c0 { + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + width: 100%; +} + +
+
+
+
+
+ sessions +
+
+ 2022-03-22T22:10:56.794Z +
+
+ 2022-03-21T22:10:56.791Z +
+
+ hosts-page-sessions +
+
+ process.start +
+
+ process.end +
+
+ process.executable +
+
+ user.name +
+
+ process.interactive +
+
+ process.pid +
+
+ host.hostname +
+
+ process.entry_leader.entry_meta.type +
+
+ process.entry_leader.entry_meta.source.ip +
+
+
+
+
+
+`; diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts new file mode 100644 index 00000000000000..ee2f2e5452298c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ColumnHeaderOptions, RowRendererId } from '../../../../common/types/timeline'; +import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; +import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; +import { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; + +export const sessionsHeaders: ColumnHeaderOptions[] = [ + { + columnHeaderType: defaultColumnHeaderType, + id: 'process.start', + initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }, + // TODO: Using event.created as an way of getting the end time of the process. (Currently endpoint doesn't populate process.end) + // event.created of a event.action with value of "end" is what we consider that to be the end time of the process + // Current action are: 'start', 'exec', 'end', so we usually have three events per process. + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.created', + display: 'process.end', + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'process.executable', + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'user.name', + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'process.interactive', + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'process.pid', + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'host.hostname', + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'process.entry_leader.entry_meta.type', + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'process.entry_leader.entry_meta.source.ip', + }, +]; + +export const sessionsDefaultModel: SubsetTimelineModel = { + ...timelineDefaults, + columns: sessionsHeaders, + defaultColumns: sessionsHeaders, + excludedRowRendererIds: Object.values(RowRendererId), +}; diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx new file mode 100644 index 00000000000000..eb1b75b81a94b1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import { waitFor, render } from '@testing-library/react'; +import { TestProviders } from '../../mock'; +import { TEST_ID, SessionsView, defaultSessionsFilter } from '.'; +import { EntityType, TimelineId } from '../../../../../timelines/common'; +import { SessionsComponentsProps } from './types'; +import { TimelineModel } from '../../../timelines/store/timeline/model'; + +jest.mock('../../../common/lib/kibana'); + +jest.mock('../../components/url_state/normalize_time_range.ts'); + +const startDate = '2022-03-22T22:10:56.794Z'; +const endDate = '2022-03-21T22:10:56.791Z'; + +const filterQuery = + '{"bool":{"must":[],"filter":[{"match_phrase":{"host.name":{"query":"ubuntu-impish"}}}],"should":[],"must_not":[]}}'; + +const testProps: SessionsComponentsProps = { + timelineId: TimelineId.hostsPageSessions, + entityType: 'sessions', + pageFilters: [], + startDate, + endDate, + filterQuery, +}; + +type Props = Partial & { + start: string; + end: string; + entityType: EntityType; +}; + +const TEST_PREFIX = 'security_solution:sessions_viewer:sessions_view'; + +const callFilters = jest.fn(); + +// creating a dummy component for testing TGrid to avoid mocking all the implementation details +// but still test if the TGrid will render properly +const SessionsViewerTGrid: React.FC = ({ columns, start, end, id, filters, entityType }) => { + useEffect(() => { + callFilters(filters); + }, [filters]); + + return ( +
+
{entityType}
+
{start}
+
{end}
+
{id}
+ {columns?.map((header) => ( +
{header.display ?? header.id}
+ ))} +
+ ); +}; + +jest.mock('../../../../../timelines/public/mock/plugin_mock.tsx', () => { + const originalModule = jest.requireActual('../../../../../timelines/public/mock/plugin_mock.tsx'); + return { + ...originalModule, + createTGridMocks: () => ({ + ...originalModule.createTGridMocks, + getTGrid: SessionsViewerTGrid, + }), + }; +}); + +describe('SessionsView', () => { + it('renders the session view', async () => { + const wrapper = render( + + + + ); + + await waitFor(() => { + expect(wrapper.queryByTestId(TEST_ID)).toBeInTheDocument(); + }); + }); + + it('renders correctly against snapshot', async () => { + const { asFragment } = render( + + + + ); + + await waitFor(() => { + expect(asFragment()).toMatchSnapshot(); + }); + }); + + it('passes in the right parameters to TGrid', async () => { + const wrapper = render( + + + + ); + await waitFor(() => { + expect(wrapper.getByTestId(`${TEST_PREFIX}:entityType`)).toHaveTextContent('sessions'); + expect(wrapper.getByTestId(`${TEST_PREFIX}:startDate`)).toHaveTextContent(startDate); + expect(wrapper.getByTestId(`${TEST_PREFIX}:endDate`)).toHaveTextContent(endDate); + expect(wrapper.getByTestId(`${TEST_PREFIX}:timelineId`)).toHaveTextContent( + 'hosts-page-sessions' + ); + }); + }); + it('passes in the right filters to TGrid', async () => { + render( + + + + ); + await waitFor(() => { + expect(callFilters).toHaveBeenCalledWith([ + { + ...defaultSessionsFilter, + query: { + ...defaultSessionsFilter.query, + bool: { + ...defaultSessionsFilter.query.bool, + filter: defaultSessionsFilter.query.bool.filter.concat(JSON.parse(filterQuery)), + }, + }, + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx new file mode 100644 index 00000000000000..3005d59bdc738b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import type { Filter } from '@kbn/es-query'; +import { SessionsComponentsProps } from './types'; +import { ESBoolQuery } from '../../../../common/typed_json'; +import { StatefulEventsViewer } from '../events_viewer'; +import { sessionsDefaultModel } from './default_headers'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; +import * as i18n from './translations'; +import { SourcererScopeName } from '../../store/sourcerer/model'; +import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; + +export const TEST_ID = 'security_solution:sessions_viewer:sessions_view'; + +export const defaultSessionsFilter: Required> = { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + // TODO: update to process.entry_leader.same_as_process once ECS is updated to support same_as_process + 'process.is_entry_leader': true, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + meta: { + alias: null, + disabled: false, + key: 'process.is_entry_leader', + negate: false, + params: {}, + type: 'boolean', + }, +}; + +const SessionsViewComponent: React.FC = ({ + timelineId, + endDate, + entityType = 'sessions', + pageFilters, + startDate, + filterQuery, +}) => { + const parsedFilterQuery: ESBoolQuery = useMemo(() => { + if (filterQuery && filterQuery !== '') { + return JSON.parse(filterQuery); + } + return {}; + }, [filterQuery]); + + const sessionsFilter = useMemo( + () => [ + { + ...defaultSessionsFilter, + query: { + ...defaultSessionsFilter.query, + bool: { + ...defaultSessionsFilter.query.bool, + filter: defaultSessionsFilter.query.bool.filter.concat(parsedFilterQuery), + }, + }, + }, + ...pageFilters, + ], + [pageFilters, parsedFilterQuery] + ); + + const ACTION_BUTTON_COUNT = 5; + const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); + + const unit = (c: number) => + c > 1 ? i18n.TOTAL_COUNT_OF_SESSIONS : i18n.SINGLE_COUNT_OF_SESSIONS; + + return ( +
+ +
+ ); +}; + +SessionsViewComponent.displayName = 'SessionsViewComponent'; + +export const SessionsView = React.memo(SessionsViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts new file mode 100644 index 00000000000000..606ae2b46fc6a1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const TOTAL_COUNT_OF_SESSIONS = i18n.translate( + 'xpack.securitySolution.sessionsView.totalCountOfSessions', + { + defaultMessage: 'sessions', + } +); + +export const SINGLE_COUNT_OF_SESSIONS = i18n.translate( + 'xpack.securitySolution.sessionsView.singleCountOfSessions', + { + defaultMessage: 'session', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/types.ts b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/types.ts new file mode 100644 index 00000000000000..3c1a73a3292b3d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Filter } from '@kbn/es-query'; +import type { EntityType } from '../../../../../timelines/common'; +import { QueryTabBodyProps } from '../../../hosts/pages/navigation/types'; +import { TimelineIdLiteral } from '../../../../common/types/timeline'; + +export interface SessionsComponentsProps extends Pick { + timelineId: TimelineIdLiteral; + pageFilters: Filter[]; + defaultFilters?: Filter[]; + entityType?: EntityType; + filterQuery?: string; +} diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx index 6c87dafef942e0..10613efc265ae4 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx @@ -28,6 +28,7 @@ const detectionAlertsTimelines = [TimelineId.detectionsPage, TimelineId.detectio const otherTimelines = [ TimelineId.hostsPageEvents, TimelineId.hostsPageExternalAlerts, + TimelineId.hostsPageSessions, TimelineId.networkPageExternalAlerts, TimelineId.active, TimelineId.casePage, diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 9882ca9f3ab3da..bd90892a43fc6c 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -88,6 +88,7 @@ export const mockGlobalState: State = { sort: { field: RiskScoreFields.riskScore, direction: Direction.desc }, severitySelection: [], }, + sessions: { activePage: 0, limit: 10 }, }, }, details: { @@ -109,6 +110,7 @@ export const mockGlobalState: State = { sort: { field: RiskScoreFields.riskScore, direction: Direction.desc }, severitySelection: [], }, + sessions: { activePage: 0, limit: 10 }, }, }, }, diff --git a/x-pack/plugins/security_solution/public/hosts/index.ts b/x-pack/plugins/security_solution/public/hosts/index.ts index cbb539f8e41074..f818a812b57a34 100644 --- a/x-pack/plugins/security_solution/public/hosts/index.ts +++ b/x-pack/plugins/security_solution/public/hosts/index.ts @@ -15,6 +15,7 @@ import { initialHostsState, hostsReducer, HostsState } from './store'; const HOST_TIMELINE_IDS: TimelineIdLiteral[] = [ TimelineId.hostsPageEvents, TimelineId.hostsPageExternalAlerts, + TimelineId.hostsPageSessions, ]; export class Hosts { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx index 142f3b922f842d..7baa72b31ae07e 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx @@ -26,6 +26,7 @@ import { UncommonProcessQueryTabBody, HostAlertsQueryTabBody, HostRiskTabBody, + SessionsTabBody, } from '../navigation'; import { TimelineId } from '../../../../common/types'; @@ -111,6 +112,9 @@ export const HostDetailsTabs = React.memo( + + + ); } diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx index 33cafd8ef21149..eb22e69e14c437 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx @@ -62,6 +62,12 @@ export const navTabsHostDetails = ({ href: getTabsOnHostDetailsUrl(hostName, HostsTableType.risk), disabled: false, }, + [HostsTableType.sessions]: { + id: HostsTableType.sessions, + name: i18n.NAVIGATION_SESSIONS_TITLE, + href: getTabsOnHostDetailsUrl(hostName, HostsTableType.sessions), + disabled: false, + }, }; if (!hasMlUserPermissions) { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts index 19975b6ad7abb7..346ac7bf3517b8 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts @@ -28,6 +28,7 @@ const TabNameMappedToI18nKey: Record = { [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, [HostsTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, [HostsTableType.risk]: i18n.NAVIGATION_HOST_RISK_TITLE, + [HostsTableType.sessions]: i18n.NAVIGATION_SESSIONS_TITLE, }; export const getBreadcrumbs = ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx index d7c615c08ec287..ed2a1ec983b20c 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx @@ -23,6 +23,7 @@ import { HostRiskScoreQueryTabBody, AuthenticationsQueryTabBody, UncommonProcessQueryTabBody, + SessionsTabBody, } from './navigation'; import { HostAlertsQueryTabBody } from './navigation/alerts_query_tab_body'; import { TimelineId } from '../../../common/types'; @@ -103,6 +104,9 @@ export const HostsTabs = memo( + + + ); } diff --git a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx index 453d6182984c19..e42be25941ea73 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx @@ -23,7 +23,8 @@ const getHostsTabPath = () => `${HostsTableType.anomalies}|` + `${HostsTableType.events}|` + `${HostsTableType.risk}|` + - `${HostsTableType.alerts})`; + `${HostsTableType.alerts}|` + + `${HostsTableType.sessions})`; const getHostDetailsTabPath = () => `${hostDetailsPagePath}/:tabName(` + @@ -32,7 +33,8 @@ const getHostDetailsTabPath = () => `${HostsTableType.anomalies}|` + `${HostsTableType.events}|` + `${HostsTableType.risk}|` + - `${HostsTableType.alerts})`; + `${HostsTableType.alerts}|` + + `${HostsTableType.sessions})`; export const HostsContainer = React.memo(() => ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx index 789273da073e94..59fb0d08f7dc76 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx @@ -66,6 +66,12 @@ export const navTabsHosts = ({ href: getTabsOnHostsUrl(HostsTableType.risk), disabled: false, }, + [HostsTableType.sessions]: { + id: HostsTableType.sessions, + name: i18n.NAVIGATION_SESSIONS_TITLE, + href: getTabsOnHostsUrl(HostsTableType.sessions), + disabled: false, + }, }; if (!hasMlUserPermissions) { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts b/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts index 3ef211e1aef33e..5ebbc61cabe416 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts @@ -11,3 +11,4 @@ export * from './uncommon_process_query_tab_body'; export * from './alerts_query_tab_body'; export * from './host_risk_tab_body'; export * from './host_risk_score_tab_body'; +export * from './sessions_tab_body'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/sessions_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/sessions_tab_body.tsx new file mode 100644 index 00000000000000..0ff47a104ca21f --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/sessions_tab_body.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { TimelineId } from '../../../../common/types/timeline'; +import { SessionsView } from '../../../common/components/sessions_viewer'; +import { filterHostExternalAlertData } from '../../../common/components/visualization_actions/utils'; +import { AlertsComponentQueryProps } from './types'; + +export const SessionsTabBody = React.memo((alertsProps: AlertsComponentQueryProps) => { + const { pageFilters, filterQuery, ...rest } = alertsProps; + const hostPageFilters = useMemo( + () => + pageFilters != null + ? [...filterHostExternalAlertData, ...pageFilters] + : filterHostExternalAlertData, + [pageFilters] + ); + + return ( + + ); +}); + +SessionsTabBody.displayName = 'SessionsTabBody'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/translations.ts b/x-pack/plugins/security_solution/public/hosts/pages/translations.ts index c2ece6c40e701f..8b92ef035405fb 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/translations.ts @@ -64,6 +64,13 @@ export const NAVIGATION_HOST_RISK_TITLE = i18n.translate( } ); +export const NAVIGATION_SESSIONS_TITLE = i18n.translate( + 'xpack.securitySolution.hosts.navigation.sessionsTitle', + { + defaultMessage: 'Sessions', + } +); + export const ERROR_FETCHING_AUTHENTICATIONS_DATA = i18n.translate( 'xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingAuthenticationsData', { diff --git a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts index 64e4d9088abd78..111b7f0e79737f 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts @@ -45,6 +45,10 @@ export const mockHostsState: HostsModel = { }, severitySelection: [], }, + [HostsTableType.sessions]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, }, }, details: { @@ -81,6 +85,10 @@ export const mockHostsState: HostsModel = { }, severitySelection: [], }, + [HostsTableType.sessions]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, }, }, }; @@ -121,6 +129,10 @@ describe('Hosts redux store', () => { field: 'risk_stats.risk_score', }, }, + [HostsTableType.sessions]: { + activePage: 0, + limit: 10, + }, }); }); @@ -158,6 +170,10 @@ describe('Hosts redux store', () => { field: 'risk_stats.risk_score', }, }, + [HostsTableType.sessions]: { + activePage: 0, + limit: 10, + }, }); }); }); diff --git a/x-pack/plugins/security_solution/public/hosts/store/model.ts b/x-pack/plugins/security_solution/public/hosts/store/model.ts index 090a469c5fb76a..09bec3643c4d44 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/model.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/model.ts @@ -25,6 +25,7 @@ export enum HostsTableType { anomalies = 'anomalies', alerts = 'externalAlerts', risk = 'hostRisk', + sessions = 'sessions', } export interface BasicQueryPaginated { @@ -50,6 +51,7 @@ export interface Queries { [HostsTableType.anomalies]: null | undefined; [HostsTableType.alerts]: BasicQueryPaginated; [HostsTableType.risk]: HostRiskScoreQuery; + [HostsTableType.sessions]: BasicQueryPaginated; } export interface GenericHostsModel { diff --git a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts index f413607b85e1c2..1ea20bf5e28716 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts @@ -62,6 +62,10 @@ export const initialHostsState: HostsState = { }, severitySelection: [], }, + [HostsTableType.sessions]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, }, }, details: { @@ -98,6 +102,10 @@ export const initialHostsState: HostsState = { }, severitySelection: [], }, + [HostsTableType.sessions]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, }, }, }; diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 0916bc73f41986..65ca8466ef00af 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -25,11 +25,11 @@ import type { import type { CasesUiStart } from '../../cases/public'; import type { SecurityPluginSetup } from '../../security/public'; import type { TimelinesUIStart } from '../../timelines/public'; +import type { SessionViewUIStart } from '../../session_view/public'; import type { ResolverPluginSetup } from './resolver/types'; import type { Inspect } from '../common/search_strategy'; import type { MlPluginSetup, MlPluginStart } from '../../ml/public'; import type { OsqueryPluginStart } from '../../osquery/public'; - import type { Detections } from './detections'; import type { Cases } from './cases'; import type { Exceptions } from './exceptions'; @@ -71,6 +71,7 @@ export interface StartPlugins { spaces?: SpacesPluginStart; dataViewFieldEditor: IndexPatternFieldEditorStart; osquery?: OsqueryPluginStart; + sessionView: SessionViewUIStart; } export type StartServices = CoreStart & diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index b1cb49b7379529..e6b5a79540458f 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -42,6 +42,7 @@ { "path": "../osquery/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, { "path": "../security/tsconfig.json" }, - { "path": "../timelines/tsconfig.json" } + { "path": "../timelines/tsconfig.json" }, + { "path": "../session_view/tsconfig.json" } ] } diff --git a/x-pack/plugins/session_view/public/index.ts b/x-pack/plugins/session_view/public/index.ts index 90043e9a691dce..3d66164bfec4f3 100644 --- a/x-pack/plugins/session_view/public/index.ts +++ b/x-pack/plugins/session_view/public/index.ts @@ -7,6 +7,8 @@ import { SessionViewPlugin } from './plugin'; +export type { SessionViewUIStart } from './types'; + export function plugin() { return new SessionViewPlugin(); } diff --git a/x-pack/plugins/session_view/public/types.ts b/x-pack/plugins/session_view/public/types.ts index a2099d11275f8f..8c989122508659 100644 --- a/x-pack/plugins/session_view/public/types.ts +++ b/x-pack/plugins/session_view/public/types.ts @@ -4,14 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ReactNode } from 'react'; +import { ReactElement, ReactNode } from 'react'; import { CoreStart } from '../../../../src/core/public'; -import { TimelinesUIStart } from '../../timelines/public'; import { ProcessEvent, Teletype } from '../common/types/process_tree'; -export type SessionViewServices = CoreStart & { - timelines: TimelinesUIStart; -}; +export type SessionViewServices = CoreStart; + +export interface SessionViewUIStart { + getSessionView: (sessionEntityId: string) => ReactElement; +} export interface SessionViewDeps { // the root node of the process tree to render. e.g process.entry.entity_id or process.session_leader.entity_id diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts index 6b204224d3d5de..f7df9bfde3d052 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts @@ -20,5 +20,6 @@ export enum TimelineEventsQueries { export const EntityType = { ALERTS: 'alerts', EVENTS: 'events', + SESSIONS: 'sessions', } as const; export type EntityType = typeof EntityType[keyof typeof EntityType]; diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index 1e12baf13c2dba..e7a0faf261318a 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -314,6 +314,7 @@ export enum TimelineId { usersPageExternalAlerts = 'users-page-external-alerts', hostsPageEvents = 'hosts-page-events', hostsPageExternalAlerts = 'hosts-page-external-alerts', + hostsPageSessions = 'hosts-page-sessions', detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', @@ -326,6 +327,7 @@ export enum TimelineId { export const TimelineIdLiteralRt = runtimeTypes.union([ runtimeTypes.literal(TimelineId.hostsPageEvents), runtimeTypes.literal(TimelineId.hostsPageExternalAlerts), + runtimeTypes.literal(TimelineId.hostsPageSessions), runtimeTypes.literal(TimelineId.detectionsRulesDetailsPage), runtimeTypes.literal(TimelineId.detectionsPage), runtimeTypes.literal(TimelineId.networkPageExternalAlerts), diff --git a/x-pack/plugins/timelines/public/store/t_grid/types.ts b/x-pack/plugins/timelines/public/store/t_grid/types.ts index dfa8331669535f..b065a5e1b1e953 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/types.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/types.ts @@ -46,6 +46,7 @@ export enum TimelineId { usersPageExternalAlerts = 'users-page-external-alerts', hostsPageEvents = 'hosts-page-events', hostsPageExternalAlerts = 'hosts-page-external-alerts', + hostsPageSessions = 'hosts-page-sessions', detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts index e764e32243c183..b0dd730c749d25 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/constants.ts @@ -199,10 +199,14 @@ export const TIMELINE_EVENTS_FIELDS = [ 'tls.server_certificate.fingerprint.sha1', 'user.domain', 'winlog.event_id', + 'process.end', + 'process.entry_leader.entry_meta.type', + 'process.entry_leader.entry_meta.source.ip', 'process.exit_code', 'process.hash.md5', 'process.hash.sha1', 'process.hash.sha256', + 'process.interactive', 'process.parent.name', 'process.parent.pid', 'process.pid', @@ -211,6 +215,7 @@ export const TIMELINE_EVENTS_FIELDS = [ 'process.args', 'process.entity_id', 'process.executable', + 'process.start', 'process.title', 'process.working_directory', 'zeek.session_id', diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts index 24e0edb5ddcc04..a5ef2f57888dab 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts @@ -64,6 +64,14 @@ export const timelineSearchStrategyProvider = ({ }) ); }; + +const timelineSessionsSearchStrategy = ({ + es, + request, + options, + deps, + queryFactory, +}: { + es: ISearchStrategy; + request: TimelineStrategyRequestType; + options: ISearchOptions; + deps: SearchStrategyDependencies; + queryFactory: TimelineFactory; +}) => { + const indices = request.defaultIndex ?? request.indexType; + + const runtimeMappings = { + // TODO: remove once ECS is updated to support process.entry_leader.same_as_process + 'process.is_entry_leader': { + type: 'boolean', + script: { + source: + "emit(doc.containsKey('process.entry_leader.entity_id') && doc['process.entry_leader.entity_id'].size() > 0 && doc['process.entity_id'].value == doc['process.entry_leader.entity_id'].value)", + }, + }, + }; + + const requestSessionLeaders = { + ...request, + defaultIndex: indices, + indexName: indices, + }; + + const dsl = queryFactory.buildDsl(requestSessionLeaders); + + const params = { ...dsl, runtime_mappings: runtimeMappings }; + + return es.search({ ...requestSessionLeaders, params }, options, deps).pipe( + map((response) => { + return { + ...response, + rawResponse: shimHitsTotal(response.rawResponse, options), + }; + }), + mergeMap((esSearchRes) => queryFactory.parse(requestSessionLeaders, esSearchRes)) + ); +};