[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>
This commit is contained in:
Paulo Henrique 2022-03-28 20:49:15 -03:00 committed by GitHub
parent 065b585757
commit 8d117ca349
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 644 additions and 10 deletions

View file

@ -118,6 +118,7 @@ export enum SecurityPageName {
users = 'users',
usersAnomalies = 'users-anomalies',
usersRisk = 'users-risk',
sessions = 'sessions',
}
export const TIMELINES_PATH = '/timelines' as const;

View file

@ -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),

View file

@ -25,7 +25,8 @@
"taskManager",
"timelines",
"triggersActionsUi",
"uiActions"
"uiActions",
"sessionView"
],
"optionalPlugins": [
"encryptedSavedObjects",

View file

@ -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`,
},
],
},
{

View file

@ -0,0 +1,107 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SessionsView renders correctly against snapshot 1`] = `
<DocumentFragment>
.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%;
}
<div
data-test-subj="security_solution:sessions_viewer:sessions_view"
>
<div
class="c0"
>
<div
class="c1"
data-test-subj="hoverVisibilityContainer"
>
<div>
<div
data-test-subj="security_solution:sessions_viewer:sessions_view:entityType"
>
sessions
</div>
<div
data-test-subj="security_solution:sessions_viewer:sessions_view:startDate"
>
2022-03-22T22:10:56.794Z
</div>
<div
data-test-subj="security_solution:sessions_viewer:sessions_view:endDate"
>
2022-03-21T22:10:56.791Z
</div>
<div
data-test-subj="security_solution:sessions_viewer:sessions_view:timelineId"
>
hosts-page-sessions
</div>
<div>
process.start
</div>
<div>
process.end
</div>
<div>
process.executable
</div>
<div>
user.name
</div>
<div>
process.interactive
</div>
<div>
process.pid
</div>
<div>
host.hostname
</div>
<div>
process.entry_leader.entry_meta.type
</div>
<div>
process.entry_leader.entry_meta.source.ip
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View file

@ -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),
};

View file

@ -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<TimelineModel> & {
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<Props> = ({ columns, start, end, id, filters, entityType }) => {
useEffect(() => {
callFilters(filters);
}, [filters]);
return (
<div>
<div data-test-subj={`${TEST_PREFIX}:entityType`}>{entityType}</div>
<div data-test-subj={`${TEST_PREFIX}:startDate`}>{start}</div>
<div data-test-subj={`${TEST_PREFIX}:endDate`}>{end}</div>
<div data-test-subj={`${TEST_PREFIX}:timelineId`}>{id}</div>
{columns?.map((header) => (
<div key={header.id}>{header.display ?? header.id}</div>
))}
</div>
);
};
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(
<TestProviders>
<SessionsView {...testProps} />
</TestProviders>
);
await waitFor(() => {
expect(wrapper.queryByTestId(TEST_ID)).toBeInTheDocument();
});
});
it('renders correctly against snapshot', async () => {
const { asFragment } = render(
<TestProviders>
<SessionsView {...testProps} />
</TestProviders>
);
await waitFor(() => {
expect(asFragment()).toMatchSnapshot();
});
});
it('passes in the right parameters to TGrid', async () => {
const wrapper = render(
<TestProviders>
<SessionsView {...testProps} />
</TestProviders>
);
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(
<TestProviders>
<SessionsView {...testProps} />
</TestProviders>
);
await waitFor(() => {
expect(callFilters).toHaveBeenCalledWith([
{
...defaultSessionsFilter,
query: {
...defaultSessionsFilter.query,
bool: {
...defaultSessionsFilter.query.bool,
filter: defaultSessionsFilter.query.bool.filter.concat(JSON.parse(filterQuery)),
},
},
},
]);
});
});
});

View file

@ -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<Pick<Filter, 'meta' | 'query'>> = {
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<SessionsComponentsProps> = ({
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 (
<div data-test-subj={TEST_ID}>
<StatefulEventsViewer
pageFilters={sessionsFilter}
defaultModel={sessionsDefaultModel}
end={endDate}
entityType={entityType}
id={timelineId}
leadingControlColumns={leadingControlColumns}
renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers}
scopeId={SourcererScopeName.default}
start={startDate}
unit={unit}
/>
</div>
);
};
SessionsViewComponent.displayName = 'SessionsViewComponent';
export const SessionsView = React.memo(SessionsViewComponent);

View file

@ -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',
}
);

View file

@ -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<QueryTabBodyProps, 'endDate' | 'startDate'> {
timelineId: TimelineIdLiteral;
pageFilters: Filter[];
defaultFilters?: Filter[];
entityType?: EntityType;
filterQuery?: string;
}

View file

@ -28,6 +28,7 @@ const detectionAlertsTimelines = [TimelineId.detectionsPage, TimelineId.detectio
const otherTimelines = [
TimelineId.hostsPageEvents,
TimelineId.hostsPageExternalAlerts,
TimelineId.hostsPageSessions,
TimelineId.networkPageExternalAlerts,
TimelineId.active,
TimelineId.casePage,

View file

@ -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 },
},
},
},

View file

@ -15,6 +15,7 @@ import { initialHostsState, hostsReducer, HostsState } from './store';
const HOST_TIMELINE_IDS: TimelineIdLiteral[] = [
TimelineId.hostsPageEvents,
TimelineId.hostsPageExternalAlerts,
TimelineId.hostsPageSessions,
];
export class Hosts {

View file

@ -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<HostDetailsTabsProps>(
<Route path={`${hostDetailsPagePath}/:tabName(${HostsTableType.risk})`}>
<HostRiskTabBody {...tabProps} />
</Route>
<Route path={`${hostDetailsPagePath}/:tabName(${HostsTableType.sessions})`}>
<SessionsTabBody {...tabProps} />
</Route>
</Switch>
);
}

View file

@ -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) {

View file

@ -28,6 +28,7 @@ const TabNameMappedToI18nKey: Record<HostsTableType, string> = {
[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 = (

View file

@ -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<HostsTabsProps>(
<Route path={`${HOSTS_PATH}/:tabName(${HostsTableType.alerts})`}>
<HostAlertsQueryTabBody {...tabProps} />
</Route>
<Route path={`${HOSTS_PATH}/:tabName(${HostsTableType.sessions})`}>
<SessionsTabBody {...tabProps} />
</Route>
</Switch>
);
}

View file

@ -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(() => (
<Switch>

View file

@ -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) {

View file

@ -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';

View file

@ -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 (
<SessionsView
entityType="sessions"
timelineId={TimelineId.hostsPageSessions}
{...rest}
pageFilters={hostPageFilters}
filterQuery={filterQuery}
/>
);
});
SessionsTabBody.displayName = 'SessionsTabBody';

View file

@ -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',
{

View file

@ -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,
},
});
});
});

View file

@ -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 {

View file

@ -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,
},
},
},
};

View file

@ -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 &

View file

@ -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" }
]
}

View file

@ -7,6 +7,8 @@
import { SessionViewPlugin } from './plugin';
export type { SessionViewUIStart } from './types';
export function plugin() {
return new SessionViewPlugin();
}

View file

@ -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

View file

@ -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];

View file

@ -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),

View file

@ -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',

View file

@ -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',

View file

@ -64,6 +64,14 @@ export const timelineSearchStrategyProvider = <T extends TimelineFactoryQueryTyp
alerting,
auditLogger: securityAuditLogger,
});
} else if (entityType != null && entityType === EntityType.SESSIONS) {
return timelineSessionsSearchStrategy({
es,
request,
options,
deps,
queryFactory,
});
} else {
return timelineSearchStrategy({ es, request, options, deps, queryFactory });
}
@ -181,3 +189,50 @@ const timelineAlertsSearchStrategy = <T extends TimelineFactoryQueryTypes>({
})
);
};
const timelineSessionsSearchStrategy = <T extends TimelineFactoryQueryTypes>({
es,
request,
options,
deps,
queryFactory,
}: {
es: ISearchStrategy;
request: TimelineStrategyRequestType<T>;
options: ISearchOptions;
deps: SearchStrategyDependencies;
queryFactory: TimelineFactory<T>;
}) => {
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))
);
};