mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
065b585757
commit
8d117ca349
34 changed files with 644 additions and 10 deletions
|
@ -118,6 +118,7 @@ export enum SecurityPageName {
|
|||
users = 'users',
|
||||
usersAnomalies = 'users-anomalies',
|
||||
usersRisk = 'users-risk',
|
||||
sessions = 'sessions',
|
||||
}
|
||||
|
||||
export const TIMELINES_PATH = '/timelines' as const;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
"taskManager",
|
||||
"timelines",
|
||||
"triggersActionsUi",
|
||||
"uiActions"
|
||||
"uiActions",
|
||||
"sessionView"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"encryptedSavedObjects",
|
||||
|
|
|
@ -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`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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),
|
||||
};
|
|
@ -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)),
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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;
|
||||
}
|
|
@ -28,6 +28,7 @@ const detectionAlertsTimelines = [TimelineId.detectionsPage, TimelineId.detectio
|
|||
const otherTimelines = [
|
||||
TimelineId.hostsPageEvents,
|
||||
TimelineId.hostsPageExternalAlerts,
|
||||
TimelineId.hostsPageSessions,
|
||||
TimelineId.networkPageExternalAlerts,
|
||||
TimelineId.active,
|
||||
TimelineId.casePage,
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ import { initialHostsState, hostsReducer, HostsState } from './store';
|
|||
const HOST_TIMELINE_IDS: TimelineIdLiteral[] = [
|
||||
TimelineId.hostsPageEvents,
|
||||
TimelineId.hostsPageExternalAlerts,
|
||||
TimelineId.hostsPageSessions,
|
||||
];
|
||||
|
||||
export class Hosts {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 &
|
||||
|
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import { SessionViewPlugin } from './plugin';
|
||||
|
||||
export type { SessionViewUIStart } from './types';
|
||||
|
||||
export function plugin() {
|
||||
return new SessionViewPlugin();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue