mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
968f350989
commit
f289a5d78b
32 changed files with 472 additions and 79 deletions
|
@ -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),
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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"]';
|
|
@ -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"]';
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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}
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -120,6 +120,7 @@ const UserRiskScoreTableComponent: React.FC<UserRiskScoreTableProps> = ({
|
|||
dispatch(
|
||||
usersActions.updateTableSorting({
|
||||
sort: newSort as RiskScoreSortField,
|
||||
tableType,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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})`;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}}]}}}',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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 />
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 />
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue