Add Events tab and External alerts tab to the User page and the User details page (#127953)

* Add Events tab to the User page and the User details page

* Add External alerts tab to the User page and the User details page

* Add cypress tests

* Add unit test to EventsQueryTabBody

* Memoize navTabs on Users page
This commit is contained in:
Pablo Machado 2022-03-24 09:31:40 +01:00 committed by GitHub
parent 968f350989
commit f289a5d78b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 472 additions and 79 deletions

View file

@ -314,6 +314,8 @@ export type TimelineWithoutExternalRefs = Omit<SavedTimeline, 'dataViewId' | 'sa
*/
export enum TimelineId {
usersPageEvents = 'users-page-events',
usersPageExternalAlerts = 'users-page-external-alerts',
hostsPageEvents = 'hosts-page-events',
hostsPageExternalAlerts = 'hosts-page-external-alerts',
detectionsRulesDetailsPage = 'detections-rules-details-page',
@ -326,6 +328,8 @@ export enum TimelineId {
}
export const TimelineIdLiteralRt = runtimeTypes.union([
runtimeTypes.literal(TimelineId.usersPageEvents),
runtimeTypes.literal(TimelineId.usersPageExternalAlerts),
runtimeTypes.literal(TimelineId.hostsPageEvents),
runtimeTypes.literal(TimelineId.hostsPageExternalAlerts),
runtimeTypes.literal(TimelineId.detectionsRulesDetailsPage),

View file

@ -0,0 +1,26 @@
/*
* 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 { EVENTS_TAB, EVENTS_TAB_CONTENT } from '../../screens/users/user_events';
import { cleanKibana } from '../../tasks/common';
import { loginAndWaitForPage } from '../../tasks/login';
import { USERS_URL } from '../../urls/navigation';
describe('Users Events tab', () => {
before(() => {
cleanKibana();
loginAndWaitForPage(USERS_URL);
});
it(`renders events tab`, () => {
cy.get(EVENTS_TAB).click();
cy.get(EVENTS_TAB_CONTENT).should('exist');
});
});

View file

@ -0,0 +1,29 @@
/*
* 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 {
EXTERNAL_ALERTS_TAB,
EXTERNAL_ALERTS_TAB_CONTENT,
} from '../../screens/users/user_external_alerts';
import { cleanKibana } from '../../tasks/common';
import { loginAndWaitForPage } from '../../tasks/login';
import { USERS_URL } from '../../urls/navigation';
describe('Users external alerts tab', () => {
before(() => {
cleanKibana();
loginAndWaitForPage(USERS_URL);
});
it(`renders external alerts tab`, () => {
cy.get(EXTERNAL_ALERTS_TAB).click();
cy.get(EXTERNAL_ALERTS_TAB_CONTENT).should('exist');
});
});

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export const EVENTS_TAB = '[data-test-subj="navigation-events"]';
export const EVENTS_TAB_CONTENT = '[data-test-subj="events-viewer-panel"]';

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export const EXTERNAL_ALERTS_TAB = '[data-test-subj="navigation-externalAlerts"]';
export const EXTERNAL_ALERTS_TAB_CONTENT = '[data-test-subj="events-viewer-panel"]';

View file

@ -0,0 +1,114 @@
/*
* 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 { render } from '@testing-library/react';
import React from 'react';
import { TimelineId } from '../../../../common/types';
import { HostsType } from '../../../hosts/store/model';
import { TestProviders } from '../../mock';
import { EventsQueryTabBody, EventsQueryTabBodyComponentProps } from './events_query_tab_body';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import * as tGridActions from '../../../../../timelines/public/store/t_grid/actions';
jest.mock('../../lib/kibana', () => {
const original = jest.requireActual('../../lib/kibana');
return {
...original,
useKibana: () => ({
services: {
...original.useKibana().services,
cases: {
ui: {
getCasesContext: jest.fn(),
},
},
},
}),
};
});
const FakeStatefulEventsViewer = () => <div>{'MockedStatefulEventsViewer'}</div>;
jest.mock('../events_viewer', () => ({ StatefulEventsViewer: FakeStatefulEventsViewer }));
jest.mock('../../containers/use_full_screen', () => ({
useGlobalFullScreen: jest.fn().mockReturnValue({
globalFullScreen: true,
}),
}));
describe('EventsQueryTabBody', () => {
const commonProps: EventsQueryTabBodyComponentProps = {
indexNames: ['test-index'],
setQuery: jest.fn(),
timelineId: TimelineId.test,
type: HostsType.page,
endDate: new Date('2000').toISOString(),
startDate: new Date('2000').toISOString(),
};
it('renders EventsViewer', () => {
const { queryByText } = render(
<TestProviders>
<EventsQueryTabBody {...commonProps} />
</TestProviders>
);
expect(queryByText('MockedStatefulEventsViewer')).toBeInTheDocument();
});
it('renders the matrix histogram when globalFullScreen is false', () => {
(useGlobalFullScreen as jest.Mock).mockReturnValue({
globalFullScreen: false,
});
const { queryByTestId } = render(
<TestProviders>
<EventsQueryTabBody {...commonProps} />
</TestProviders>
);
expect(queryByTestId('eventsHistogramQueryPanel')).toBeInTheDocument();
});
it("doesn't render the matrix histogram when globalFullScreen is true", () => {
(useGlobalFullScreen as jest.Mock).mockReturnValue({
globalFullScreen: true,
});
const { queryByTestId } = render(
<TestProviders>
<EventsQueryTabBody {...commonProps} />
</TestProviders>
);
expect(queryByTestId('eventsHistogramQueryPanel')).not.toBeInTheDocument();
});
it('deletes query when unmouting', () => {
const mockDeleteQuery = jest.fn();
const { unmount } = render(
<TestProviders>
<EventsQueryTabBody {...commonProps} deleteQuery={mockDeleteQuery} />
</TestProviders>
);
unmount();
expect(mockDeleteQuery).toHaveBeenCalled();
});
it('initializes t-grid', () => {
const spy = jest.spyOn(tGridActions, 'initializeTGridSettings');
render(
<TestProviders>
<EventsQueryTabBody {...commonProps} />
</TestProviders>
);
expect(spy).toHaveBeenCalled();
});
});

View file

@ -8,27 +8,28 @@
import React, { useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { Filter } from '@kbn/es-query';
import { TimelineId } from '../../../../common/types/timeline';
import { StatefulEventsViewer } from '../../../common/components/events_viewer';
import { StatefulEventsViewer } from '../events_viewer';
import { timelineActions } from '../../../timelines/store/timeline';
import { HostsComponentsQueryProps } from './types';
import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model';
import {
MatrixHistogramOption,
MatrixHistogramConfigs,
} from '../../../common/components/matrix_histogram/types';
import { MatrixHistogram } from '../../../common/components/matrix_histogram';
import { useGlobalFullScreen } from '../../../common/containers/use_full_screen';
import * as i18n from '../translations';
import { eventsDefaultModel } from '../events_viewer/default_model';
import { MatrixHistogram } from '../matrix_histogram';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import * as i18n from '../../../hosts/pages/translations';
import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution';
import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
import { defaultCellActions } from '../../../common/lib/cell_actions/default_cell_actions';
import { getEventsHistogramLensAttributes } from '../../../common/components/visualization_actions/lens_attributes/hosts/events';
import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions';
import { GlobalTimeArgs } from '../../containers/use_global_time';
import { MatrixHistogramConfigs, MatrixHistogramOption } from '../matrix_histogram/types';
import { QueryTabBodyProps as UserQueryTabBodyProps } from '../../../users/pages/navigation/types';
import { QueryTabBodyProps as HostQueryTabBodyProps } from '../../../hosts/pages/navigation/types';
const EVENTS_HISTOGRAM_ID = 'eventsHistogramQuery';
@ -61,7 +62,17 @@ export const histogramConfigs: MatrixHistogramConfigs = {
getLensAttributes: getEventsHistogramLensAttributes,
};
const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({
type QueryTabBodyProps = UserQueryTabBodyProps | HostQueryTabBodyProps;
export type EventsQueryTabBodyComponentProps = QueryTabBodyProps & {
deleteQuery?: GlobalTimeArgs['deleteQuery'];
indexNames: string[];
pageFilters?: Filter[];
setQuery: GlobalTimeArgs['setQuery'];
timelineId: TimelineId;
};
const EventsQueryTabBodyComponent: React.FC<EventsQueryTabBodyComponentProps> = ({
deleteQuery,
endDate,
filterQuery,
@ -69,6 +80,7 @@ const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({
pageFilters,
setQuery,
startDate,
timelineId,
}) => {
const dispatch = useDispatch();
const { globalFullScreen } = useGlobalFullScreen();
@ -78,7 +90,7 @@ const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({
useEffect(() => {
dispatch(
timelineActions.initializeTGridSettings({
id: TimelineId.hostsPageEvents,
id: timelineId,
defaultColumns: eventsDefaultModel.columns.map((c) =>
!tGridEnabled && c.initialWidth == null
? {
@ -89,7 +101,7 @@ const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({
),
})
);
}, [dispatch, tGridEnabled]);
}, [dispatch, tGridEnabled, timelineId]);
useEffect(() => {
return () => {
@ -119,7 +131,7 @@ const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({
defaultModel={eventsDefaultModel}
end={endDate}
entityType="events"
id={TimelineId.hostsPageEvents}
id={timelineId}
leadingControlColumns={leadingControlColumns}
pageFilters={pageFilters}
renderCellValue={DefaultCellRenderer}

View file

@ -203,7 +203,6 @@ export const mockGlobalState: State = {
[usersModel.UsersTableType.allUsers]: {
activePage: 0,
limit: 10,
// TODO sort: { field: RiskScoreFields.riskScore, direction: Direction.desc },
},
[usersModel.UsersTableType.anomalies]: null,
[usersModel.UsersTableType.risk]: {
@ -215,11 +214,15 @@ export const mockGlobalState: State = {
},
severitySelection: [],
},
[usersModel.UsersTableType.events]: { activePage: 0, limit: 10 },
[usersModel.UsersTableType.alerts]: { activePage: 0, limit: 10 },
},
},
details: {
queries: {
[usersModel.UsersTableType.anomalies]: null,
[usersModel.UsersTableType.events]: { activePage: 0, limit: 10 },
[usersModel.UsersTableType.alerts]: { activePage: 0, limit: 10 },
},
},
},

View file

@ -15,6 +15,7 @@ import { HostsTableType } from '../../store/model';
import { AnomaliesQueryTabBody } from '../../../common/containers/anomalies/anomalies_query_tab_body';
import { useGlobalTime } from '../../../common/containers/use_global_time';
import { AnomaliesHostTable } from '../../../common/components/ml/tables/anomalies_host_table';
import { EventsQueryTabBody } from '../../../common/components/events_tab/events_query_tab_body';
import { HostDetailsTabsProps } from './types';
import { type } from './utils';
@ -23,10 +24,10 @@ import {
HostsQueryTabBody,
AuthenticationsQueryTabBody,
UncommonProcessQueryTabBody,
EventsQueryTabBody,
HostAlertsQueryTabBody,
HostRiskTabBody,
} from '../navigation';
import { TimelineId } from '../../../../common/types';
export const HostDetailsTabs = React.memo<HostDetailsTabsProps>(
({
@ -98,7 +99,11 @@ export const HostDetailsTabs = React.memo<HostDetailsTabsProps>(
</Route>
<Route path={`${hostDetailsPagePath}/:tabName(${HostsTableType.events})`}>
<EventsQueryTabBody {...tabProps} pageFilters={pageFilters} />
<EventsQueryTabBody
{...tabProps}
pageFilters={pageFilters}
timelineId={TimelineId.hostsPageEvents}
/>
</Route>
<Route path={`${hostDetailsPagePath}/:tabName(${HostsTableType.alerts})`}>
<HostAlertsQueryTabBody {...tabProps} pageFilters={pageFilters} />

View file

@ -15,15 +15,17 @@ import { HostsTableType } from '../store/model';
import { AnomaliesQueryTabBody } from '../../common/containers/anomalies/anomalies_query_tab_body';
import { AnomaliesHostTable } from '../../common/components/ml/tables/anomalies_host_table';
import { UpdateDateRange } from '../../common/components/charts/common';
import { EventsQueryTabBody } from '../../common/components/events_tab/events_query_tab_body';
import { HOSTS_PATH } from '../../../common/constants';
import {
HostsQueryTabBody,
HostRiskScoreQueryTabBody,
AuthenticationsQueryTabBody,
UncommonProcessQueryTabBody,
EventsQueryTabBody,
} from './navigation';
import { HostAlertsQueryTabBody } from './navigation/alerts_query_tab_body';
import { TimelineId } from '../../../common/types';
export const HostsTabs = memo<HostsTabsProps>(
({
@ -96,7 +98,7 @@ export const HostsTabs = memo<HostsTabsProps>(
<AnomaliesQueryTabBody {...tabProps} AnomaliesTableComponent={AnomaliesHostTable} />
</Route>
<Route path={`${HOSTS_PATH}/:tabName(${HostsTableType.events})`}>
<EventsQueryTabBody {...tabProps} />
<EventsQueryTabBody {...tabProps} timelineId={TimelineId.hostsPageEvents} />
</Route>
<Route path={`${HOSTS_PATH}/:tabName(${HostsTableType.alerts})`}>
<HostAlertsQueryTabBody {...tabProps} />

View file

@ -6,7 +6,6 @@
*/
export * from './authentications_query_tab_body';
export * from './events_query_tab_body';
export * from './hosts_query_tab_body';
export * from './uncommon_process_query_tab_body';
export * from './alerts_query_tab_body';

View file

@ -22,10 +22,12 @@ import {
MatrixHistogramConfigs,
MatrixHistogramOption,
} from '../../../common/components/matrix_histogram/types';
import { eventsStackByOptions } from '../../../hosts/pages/navigation';
import { convertToBuildEsQuery } from '../../../common/lib/keury';
import { useKibana, useUiSetting$ } from '../../../common/lib/kibana';
import { histogramConfigs } from '../../../hosts/pages/navigation/events_query_tab_body';
import {
eventsStackByOptions,
histogramConfigs,
} from '../../../common/components/events_tab/events_query_tab_body';
import { getEsQueryConfig } from '../../../../../../../src/plugins/data/common';
import { HostsTableType } from '../../../hosts/store/model';
import { InputsModelId } from '../../../common/store/inputs/constants';

View file

@ -120,6 +120,7 @@ const UserRiskScoreTableComponent: React.FC<UserRiskScoreTableProps> = ({
dispatch(
usersActions.updateTableSorting({
sort: newSort as RiskScoreSortField,
tableType,
})
);
}

View file

@ -10,6 +10,6 @@ import { UsersTableType } from '../store/model';
export const usersDetailsPagePath = `${USERS_PATH}/:detailName`;
export const usersTabPath = `${USERS_PATH}/:tabName(${UsersTableType.allUsers}|${UsersTableType.anomalies}|${UsersTableType.risk})`;
export const usersTabPath = `${USERS_PATH}/:tabName(${UsersTableType.allUsers}|${UsersTableType.anomalies}|${UsersTableType.risk}|${UsersTableType.events}|${UsersTableType.alerts})`;
export const usersDetailsTabPath = `${usersDetailsPagePath}/:tabName(${UsersTableType.anomalies})`;
export const usersDetailsTabPath = `${usersDetailsPagePath}/:tabName(${UsersTableType.anomalies}|${UsersTableType.events}|${UsersTableType.alerts})`;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Route, Switch } from 'react-router-dom';
import { UsersTableType } from '../../store/model';
@ -16,6 +16,10 @@ import { scoreIntervalToDateTime } from '../../../common/components/ml/score/sco
import { UpdateDateRange } from '../../../common/components/charts/common';
import { Anomaly } from '../../../common/components/ml/types';
import { usersDetailsPagePath } from '../constants';
import { TimelineId } from '../../../../common/types';
import { EventsQueryTabBody } from '../../../common/components/events_tab/events_query_tab_body';
import { AlertsView } from '../../../common/components/alerts_viewer';
import { filterUserExternalAlertData } from './helpers';
export const UsersDetailsTabs = React.memo<UsersDetailsTabsProps>(
({
@ -29,6 +33,7 @@ export const UsersDetailsTabs = React.memo<UsersDetailsTabsProps>(
type,
setAbsoluteRangeDatePicker,
detailName,
pageFilters,
}) => {
const narrowDateRange = useCallback(
(score: Anomaly, interval: string) => {
@ -57,6 +62,14 @@ export const UsersDetailsTabs = React.memo<UsersDetailsTabsProps>(
[setAbsoluteRangeDatePicker]
);
const alertsPageFilters = useMemo(
() =>
pageFilters != null
? [...filterUserExternalAlertData, ...pageFilters]
: filterUserExternalAlertData,
[pageFilters]
);
const tabProps = {
deleteQuery,
endDate: to,
@ -76,6 +89,22 @@ export const UsersDetailsTabs = React.memo<UsersDetailsTabsProps>(
<Route path={`${usersDetailsPagePath}/:tabName(${UsersTableType.anomalies})`}>
<AnomaliesQueryTabBody {...tabProps} AnomaliesTableComponent={AnomaliesUserTable} />
</Route>
<Route path={`${usersDetailsPagePath}/:tabName(${UsersTableType.events})`}>
<EventsQueryTabBody
{...tabProps}
pageFilters={pageFilters}
timelineId={TimelineId.usersPageEvents}
/>
</Route>
<Route path={`${usersDetailsPagePath}/:tabName(${UsersTableType.alerts})`}>
<AlertsView
entityType="events"
timelineId={TimelineId.usersPageExternalAlerts}
pageFilters={alertsPageFilters}
{...tabProps}
/>
</Route>
</Switch>
);
}

View file

@ -30,3 +30,35 @@ export const getUsersDetailsPageFilters = (userName: string): Filter[] => [
},
},
];
export const filterUserExternalAlertData: Filter[] = [
{
query: {
bool: {
filter: [
{
bool: {
should: [
{
exists: {
field: 'user.name',
},
},
],
minimum_should_match: 1,
},
},
],
},
},
meta: {
alias: '',
disabled: false,
key: 'bool',
negate: false,
type: 'custom',
value:
'{"query": {"bool": {"filter": [{"bool": {"should": [{"exists": {"field": "user.name"}}],"minimum_should_match": 1}}]}}}',
},
},
];

View file

@ -50,6 +50,8 @@ import { useQueryInspector } from '../../../common/components/page/manage_query'
import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime';
import { getCriteriaFromUsersType } from '../../../common/components/ml/criteria/get_criteria_from_users_type';
import { UsersType } from '../../store/model';
import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions';
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
const QUERY_ID = 'UsersDetailsQueryId';
const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
@ -110,6 +112,8 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
skip: selectedPatterns.length === 0,
});
const capabilities = useMlCapabilities();
useQueryInspector({ setQuery, deleteQuery, refetch, inspect, loading, queryId: QUERY_ID });
return (
@ -165,7 +169,9 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
<EuiSpacer />
<SecuritySolutionTabNavigation navTabs={navTabsUsersDetails(detailName)} />
<SecuritySolutionTabNavigation
navTabs={navTabsUsersDetails(detailName, hasMlUserPermissions(capabilities))}
/>
<EuiSpacer />

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { omit } from 'lodash/fp';
import * as i18n from '../translations';
import { UsersDetailsNavTab } from './types';
import { UsersTableType } from '../../store/model';
@ -13,13 +14,32 @@ import { USERS_PATH } from '../../../../common/constants';
const getTabsOnUsersDetailsUrl = (userName: string, tabName: UsersTableType) =>
`${USERS_PATH}/${userName}/${tabName}`;
export const navTabsUsersDetails = (userName: string): UsersDetailsNavTab => {
return {
export const navTabsUsersDetails = (
userName: string,
hasMlUserPermissions: boolean
): UsersDetailsNavTab => {
const userDetailsNavTabs = {
[UsersTableType.anomalies]: {
id: UsersTableType.anomalies,
name: i18n.NAVIGATION_ANOMALIES_TITLE,
href: getTabsOnUsersDetailsUrl(userName, UsersTableType.anomalies),
disabled: false,
},
[UsersTableType.events]: {
id: UsersTableType.events,
name: i18n.NAVIGATION_EVENTS_TITLE,
href: getTabsOnUsersDetailsUrl(userName, UsersTableType.events),
disabled: false,
},
[UsersTableType.alerts]: {
id: UsersTableType.alerts,
name: i18n.NAVIGATION_ALERTS_TITLE,
href: getTabsOnUsersDetailsUrl(userName, UsersTableType.alerts),
disabled: false,
},
};
return hasMlUserPermissions
? userDetailsNavTabs
: omit([UsersTableType.anomalies], userDetailsNavTabs);
};

View file

@ -44,7 +44,15 @@ export type UsersDetailsComponentProps = UsersDetailsComponentReduxProps &
UsersDetailsComponentDispatchProps &
UsersQueryProps;
type KeyUsersDetailsNavTab = UsersTableType.anomalies;
export type KeyUsersDetailsNavTabWithoutMlPermission = UsersTableType.events &
UsersTableType.alerts;
type KeyUsersDetailsNavTabWithMlPermission = KeyUsersDetailsNavTabWithoutMlPermission &
UsersTableType.anomalies;
type KeyUsersDetailsNavTab =
| KeyUsersDetailsNavTabWithoutMlPermission
| KeyUsersDetailsNavTabWithMlPermission;
export type UsersDetailsNavTab = Record<KeyUsersDetailsNavTab, NavTab>;

View file

@ -24,6 +24,8 @@ const TabNameMappedToI18nKey: Record<UsersTableType, string> = {
[UsersTableType.allUsers]: i18n.NAVIGATION_ALL_USERS_TITLE,
[UsersTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE,
[UsersTableType.risk]: i18n.NAVIGATION_RISK_TITLE,
[UsersTableType.events]: i18n.NAVIGATION_EVENTS_TITLE,
[UsersTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE,
};
export const getBreadcrumbs = (

View file

@ -12,44 +12,53 @@ import { UsersTableType } from '../store/model';
import { Users } from './users';
import { UsersDetails } from './details';
import { usersDetailsPagePath, usersDetailsTabPath, usersTabPath } from './constants';
import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities';
import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions';
export const UsersContainer = React.memo(() => (
<Switch>
<Route path={usersTabPath}>
<Users />
</Route>
export const UsersContainer = React.memo(() => {
const capabilities = useMlCapabilities();
const hasMlPermissions = hasMlUserPermissions(capabilities);
<Route
path={usersDetailsTabPath}
render={({
match: {
params: { detailName },
},
}) => <UsersDetails usersDetailsPagePath={usersDetailsPagePath} detailName={detailName} />}
/>
<Route
path={usersDetailsPagePath}
render={({
match: {
params: { detailName },
},
location: { search = '' },
}) => (
<Redirect
to={{
pathname: `${USERS_PATH}/${detailName}/${UsersTableType.anomalies}`,
search,
}}
/>
)}
/>
<Route
path={USERS_PATH}
render={({ location: { search = '' } }) => (
<Redirect to={{ pathname: `${USERS_PATH}/${UsersTableType.allUsers}`, search }} />
)}
/>
</Switch>
));
return (
<Switch>
<Route path={usersTabPath}>
<Users />
</Route>
<Route
path={usersDetailsTabPath}
render={({
match: {
params: { detailName },
},
}) => <UsersDetails usersDetailsPagePath={usersDetailsPagePath} detailName={detailName} />}
/>
<Route
path={usersDetailsPagePath}
render={({
match: {
params: { detailName },
},
location: { search = '' },
}) => (
<Redirect
to={{
pathname: `${USERS_PATH}/${detailName}/${
hasMlPermissions ? UsersTableType.anomalies : UsersTableType.events
}`,
search,
}}
/>
)}
/>
<Route
path={USERS_PATH}
render={({ location: { search = '' } }) => (
<Redirect to={{ pathname: `${USERS_PATH}/${UsersTableType.allUsers}`, search }} />
)}
/>
</Switch>
);
});
UsersContainer.displayName = 'UsersContainer';

View file

@ -38,6 +38,18 @@ export const navTabsUsers = (
href: getTabsOnUsersUrl(UsersTableType.risk),
disabled: false,
},
[UsersTableType.events]: {
id: UsersTableType.events,
name: i18n.NAVIGATION_EVENTS_TITLE,
href: getTabsOnUsersUrl(UsersTableType.events),
disabled: false,
},
[UsersTableType.alerts]: {
id: UsersTableType.alerts,
name: i18n.NAVIGATION_ALERTS_TITLE,
href: getTabsOnUsersUrl(UsersTableType.alerts),
disabled: false,
},
};
if (!hasMlUserPermissions) {

View file

@ -42,12 +42,11 @@ export const AllUsersQueryTabBody = ({
indexNames,
skip: querySkip,
startDate,
// TODO Fix me
// TODO Move authentication table and hook store to 'public/common' folder when 'usersEnabled' FF is removed
// @ts-ignore
type,
deleteQuery,
});
// TODO Use a different table
return (
<AuthenticationTableManage
data={authentications}
@ -65,7 +64,7 @@ export const AllUsersQueryTabBody = ({
totalCount={totalCount}
docValueFields={docValueFields}
indexNames={indexNames}
// TODO Fix me
// TODO Move authentication table and hook store to 'public/common' folder when 'usersEnabled' FF is removed
// @ts-ignore
type={type}
/>

View file

@ -10,7 +10,11 @@ import { ESTermQuery } from '../../../../common/typed_json';
import { DocValueFields } from '../../../../../timelines/common';
import { NavTab } from '../../../common/components/navigation/types';
type KeyUsersNavTabWithoutMlPermission = UsersTableType.allUsers & UsersTableType.risk;
type KeyUsersNavTabWithoutMlPermission = UsersTableType.allUsers &
UsersTableType.risk &
UsersTableType.events &
UsersTableType.alerts;
type KeyUsersNavTabWithMlPermission = KeyUsersNavTabWithoutMlPermission & UsersTableType.anomalies;
type KeyUsersNavTab = KeyUsersNavTabWithoutMlPermission | KeyUsersNavTabWithMlPermission;

View file

@ -31,3 +31,17 @@ export const NAVIGATION_RISK_TITLE = i18n.translate(
defaultMessage: 'Users by risk',
}
);
export const NAVIGATION_EVENTS_TITLE = i18n.translate(
'xpack.securitySolution.users.navigation.eventsTitle',
{
defaultMessage: 'Events',
}
);
export const NAVIGATION_ALERTS_TITLE = i18n.translate(
'xpack.securitySolution.users.navigation.alertsTitle',
{
defaultMessage: 'External alerts',
}
);

View file

@ -162,6 +162,10 @@ const UsersComponent = () => {
const capabilities = useMlCapabilities();
const riskyUsersFeatureEnabled = useIsExperimentalFeatureEnabled('riskyUsersEnabled');
const navTabs = useMemo(
() => navTabsUsers(hasMlUserPermissions(capabilities), riskyUsersFeatureEnabled),
[capabilities, riskyUsersFeatureEnabled]
);
return (
<>
@ -197,9 +201,7 @@ const UsersComponent = () => {
<EuiSpacer />
<SecuritySolutionTabNavigation
navTabs={navTabsUsers(hasMlUserPermissions(capabilities), riskyUsersFeatureEnabled)}
/>
<SecuritySolutionTabNavigation navTabs={navTabs} />
<EuiSpacer />

View file

@ -19,6 +19,9 @@ import { scoreIntervalToDateTime } from '../../common/components/ml/score/score_
import { UpdateDateRange } from '../../common/components/charts/common';
import { UserRiskScoreQueryTabBody } from './navigation/user_risk_score_tab_body';
import { EventsQueryTabBody } from '../../common/components/events_tab/events_query_tab_body';
import { TimelineId } from '../../../common/types';
import { AlertsView } from '../../common/components/alerts_viewer';
export const UsersTabs = memo<UsersTabsProps>(
({
@ -83,6 +86,17 @@ export const UsersTabs = memo<UsersTabsProps>(
<Route path={`${USERS_PATH}/:tabName(${UsersTableType.risk})`}>
<UserRiskScoreQueryTabBody {...tabProps} />
</Route>
<Route path={`${USERS_PATH}/:tabName(${UsersTableType.events})`}>
<EventsQueryTabBody {...tabProps} timelineId={TimelineId.usersPageEvents} />
</Route>
<Route path={`${USERS_PATH}/:tabName(${UsersTableType.alerts})`}>
<AlertsView
entityType="events"
timelineId={TimelineId.usersPageExternalAlerts}
pageFilters={[]}
{...tabProps}
/>
</Route>
</Switch>
);
}

View file

@ -31,6 +31,7 @@ export const updateTableActivePage = actionCreator<{
export const updateTableSorting = actionCreator<{
sort: RiskScoreSortField;
tableType: usersModel.UsersTableType.risk;
}>('UPDATE_USERS_SORTING');
export const updateUserRiskScoreSeverityFilter = actionCreator<{

View file

@ -16,6 +16,8 @@ export enum UsersTableType {
allUsers = 'allUsers',
anomalies = 'anomalies',
risk = 'userRisk',
events = 'events',
alerts = 'externalAlerts',
}
export type AllUsersTables = UsersTableType;
@ -36,10 +38,14 @@ export interface UsersQueries {
[UsersTableType.allUsers]: AllUsersQuery;
[UsersTableType.anomalies]: null | undefined;
[UsersTableType.risk]: UsersRiskScoreQuery;
[UsersTableType.events]: BasicQueryPaginated;
[UsersTableType.alerts]: BasicQueryPaginated;
}
export interface UserDetailsQueries {
[UsersTableType.anomalies]: null | undefined;
[UsersTableType.events]: BasicQueryPaginated;
[UsersTableType.alerts]: BasicQueryPaginated;
}
export interface UsersPageModel {

View file

@ -37,11 +37,27 @@ export const initialUsersState: UsersModel = {
severitySelection: [],
},
[UsersTableType.anomalies]: null,
[UsersTableType.events]: {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
limit: DEFAULT_TABLE_LIMIT,
},
[UsersTableType.alerts]: {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
limit: DEFAULT_TABLE_LIMIT,
},
},
},
details: {
queries: {
[UsersTableType.anomalies]: null,
[UsersTableType.events]: {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
limit: DEFAULT_TABLE_LIMIT,
},
[UsersTableType.alerts]: {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
limit: DEFAULT_TABLE_LIMIT,
},
},
},
};
@ -80,14 +96,14 @@ export const usersReducer = reducerWithInitialState(initialUsersState)
},
},
}))
.case(updateTableSorting, (state, { sort }) => ({
.case(updateTableSorting, (state, { sort, tableType }) => ({
...state,
page: {
...state.page,
queries: {
...state.page.queries,
[UsersTableType.risk]: {
...state.page.queries[UsersTableType.risk],
[tableType]: {
...state.page.queries[tableType],
sort,
},
},

View file

@ -310,6 +310,8 @@ export type SavedTimelineNote = runtimeTypes.TypeOf<typeof SavedTimelineRuntimeT
*/
export enum TimelineId {
usersPageEvents = 'users-page-events',
usersPageExternalAlerts = 'users-page-external-alerts',
hostsPageEvents = 'hosts-page-events',
hostsPageExternalAlerts = 'hosts-page-external-alerts',
detectionsRulesDetailsPage = 'detections-rules-details-page',

View file

@ -42,6 +42,8 @@ export interface TimelineState {
}
export enum TimelineId {
usersPageEvents = 'users-page-events',
usersPageExternalAlerts = 'users-page-external-alerts',
hostsPageEvents = 'hosts-page-events',
hostsPageExternalAlerts = 'hosts-page-external-alerts',
detectionsRulesDetailsPage = 'detections-rules-details-page',