mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Entity Analytics][Privmon] Manage data sources page (#225053)
## Summary This PR adds a management page to the current privmon dashboard to facilitate adding data sources *after* the initial onboarding flow --------- Co-authored-by: jaredburgettelastic <jared.burgett@elastic.co> Co-authored-by: Pablo Machado <pablo.nevesmachado@elastic.co>
This commit is contained in:
parent
0c377fafa8
commit
e7d6e441de
8 changed files with 325 additions and 8 deletions
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiLoadingSpinner,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useBoolean } from '@kbn/react-hooks';
|
||||
import { useGetLatestCSVPrivilegedUserUploadQuery } from './hooks/manage_data_sources_query_hooks';
|
||||
import { UploadPrivilegedUsersModal } from '../privileged_user_monitoring_onboarding/components/file_uploader';
|
||||
import type { AddDataSourceResult } from '.';
|
||||
import { PreferenceFormattedDate } from '../../../common/components/formatted_date';
|
||||
|
||||
export const CsvUploadManageDataSource = ({
|
||||
setAddDataSourceResult,
|
||||
namespace,
|
||||
}: {
|
||||
setAddDataSourceResult: (result: AddDataSourceResult) => void;
|
||||
namespace: string;
|
||||
}) => {
|
||||
const [isImportFileModalVisible, { on: showImportFileModal, off: closeImportFileModal }] =
|
||||
useBoolean(false);
|
||||
|
||||
const { latestTimestamp, isLoading, isError, refetch } =
|
||||
useGetLatestCSVPrivilegedUserUploadQuery(namespace);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup alignItems={'flexStart'} direction={'column'}>
|
||||
<EuiFlexGroup gutterSize={'s'} alignItems={'center'}>
|
||||
<EuiIcon size={'l'} type={'importAction'} />
|
||||
<EuiText>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.file"
|
||||
defaultMessage="File"
|
||||
/>
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexGroup>
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.file.text"
|
||||
defaultMessage="CSV file exported from your user management tool. Only one file can be added as a data source, and privileged users previously uploaded through CSV will be overwritten."
|
||||
/>
|
||||
</p>
|
||||
{isError && (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.file.retrievalError"
|
||||
defaultMessage="There was an error retrieving previous CSV uploads."
|
||||
/>
|
||||
}
|
||||
color={'danger'}
|
||||
/>
|
||||
)}
|
||||
{isLoading && <EuiLoadingSpinner size="l" />}
|
||||
{!isLoading && !isError && !latestTimestamp && (
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.file.noFilesAdded"
|
||||
defaultMessage="No files added"
|
||||
/>
|
||||
</h4>
|
||||
)}
|
||||
{latestTimestamp && (
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.file.lastUpdatedTimestamp"
|
||||
defaultMessage="Last uploaded: "
|
||||
/>
|
||||
<PreferenceFormattedDate value={new Date(latestTimestamp)} />
|
||||
</h4>
|
||||
)}
|
||||
</EuiText>
|
||||
<EuiButton
|
||||
disabled={isError || isLoading}
|
||||
onClick={showImportFileModal}
|
||||
fullWidth={false}
|
||||
iconType={'plusInCircle'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.indices.text"
|
||||
defaultMessage="Import file"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
{isImportFileModalVisible && (
|
||||
<UploadPrivilegedUsersModal
|
||||
onClose={closeImportFileModal}
|
||||
onImport={async (userCount: number) => {
|
||||
closeImportFileModal();
|
||||
setAddDataSourceResult({ successful: true, userCount });
|
||||
await refetch();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import { getESQLResults } from '@kbn/esql-utils';
|
||||
import { getPrivilegedMonitorUsersIndex } from '../../../../../common/entity_analytics/privilege_monitoring/constants';
|
||||
import { esqlResponseToRecords } from '../../../../common/utils/esql';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
||||
const getLatestCSVPrivilegedUserUploadQuery = (namespace: string) => {
|
||||
return `FROM ${getPrivilegedMonitorUsersIndex(namespace)}
|
||||
| WHERE labels.sources == "csv"
|
||||
| STATS latest_timestamp = MAX(@timestamp)`;
|
||||
};
|
||||
|
||||
export const useGetLatestCSVPrivilegedUserUploadQuery = (namespace: string) => {
|
||||
const search = useKibana().services.data.search.search;
|
||||
|
||||
const { isLoading, data, isError, refetch } = useQuery([], async ({ signal }) => {
|
||||
return esqlResponseToRecords<{ latest_timestamp: string }>(
|
||||
(
|
||||
await getESQLResults({
|
||||
esqlQuery: getLatestCSVPrivilegedUserUploadQuery(namespace),
|
||||
search,
|
||||
signal,
|
||||
})
|
||||
)?.response
|
||||
);
|
||||
});
|
||||
const latestTimestamp = data ? data.find(Boolean)?.latest_timestamp : undefined;
|
||||
return {
|
||||
latestTimestamp,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useState } from 'react';
|
||||
import { useBoolean } from '@kbn/react-hooks';
|
||||
import { CsvUploadManageDataSource } from './csv_upload_manage_data_source';
|
||||
import { HeaderPage } from '../../../common/components/header_page';
|
||||
import { useSpaceId } from '../../../common/hooks/use_space_id';
|
||||
import { IndexSelectorModal } from '../privileged_user_monitoring_onboarding/components/select_index_modal';
|
||||
import { useFetchPrivilegedUserIndices } from '../privileged_user_monitoring_onboarding/hooks/use_fetch_privileged_user_indices';
|
||||
|
||||
export interface AddDataSourceResult {
|
||||
successful: boolean;
|
||||
userCount: number;
|
||||
}
|
||||
|
||||
export const PrivilegedUserMonitoringManageDataSources = ({
|
||||
onBackToDashboardClicked,
|
||||
onDone,
|
||||
}: {
|
||||
onBackToDashboardClicked: () => void;
|
||||
onDone: (userCount: number) => void;
|
||||
}) => {
|
||||
const spaceId = useSpaceId();
|
||||
const [addDataSourceResult, setAddDataSourceResult] = useState<AddDataSourceResult | undefined>();
|
||||
const [isIndexModalOpen, { on: showIndexModal, off: hideIndexModal }] = useBoolean(false);
|
||||
|
||||
const { data: indices = [], isFetching } = useFetchPrivilegedUserIndices(undefined);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty
|
||||
flush={'left'}
|
||||
iconType={'arrowLeft'}
|
||||
iconSide={'left'}
|
||||
onClick={onBackToDashboardClicked}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.back"
|
||||
defaultMessage={'Back to privileged user monitoring'}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<HeaderPage
|
||||
border={true}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.title"
|
||||
defaultMessage={'Manage data sources'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{addDataSourceResult?.successful && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.successMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'New data source of privileged users successfully set up: {userCount} users added',
|
||||
values: { userCount: addDataSourceResult.userCount },
|
||||
}
|
||||
)}
|
||||
color="success"
|
||||
iconType={'check'}
|
||||
/>
|
||||
<EuiSpacer size={'l'} />
|
||||
</>
|
||||
)}
|
||||
<EuiFlexGroup alignItems={'flexStart'} direction={'column'}>
|
||||
<EuiFlexGroup gutterSize={'s'} alignItems={'center'}>
|
||||
<EuiIcon size={'l'} type={'indexOpen'} />
|
||||
<EuiText>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.indices"
|
||||
defaultMessage="Indices"
|
||||
/>
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexGroup>
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.indices.infoText"
|
||||
defaultMessage="One or more indices containing the user.name field. All user names in the indices, specified in the user.name field, will be defined as privileged users."
|
||||
/>
|
||||
</p>
|
||||
|
||||
<h4>
|
||||
{isFetching && <EuiLoadingSpinner size="m" data-test-subj="loading-indices-spinner" />}
|
||||
{indices.length === 0 && (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.indices.noIndicesAdded"
|
||||
defaultMessage="No indices added"
|
||||
/>
|
||||
)}
|
||||
{indices.length > 0 && (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.indices.numIndicesAdded"
|
||||
defaultMessage="{indexCount, plural, one {# index} other {# indices}} added"
|
||||
values={{ indexCount: indices.length }}
|
||||
/>
|
||||
)}
|
||||
</h4>
|
||||
</EuiText>
|
||||
<EuiButton fullWidth={false} iconType={'plusInCircle'} onClick={showIndexModal}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.manageDataSources.indices.btnText"
|
||||
defaultMessage="Select index"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size={'xxl'} />
|
||||
{spaceId && (
|
||||
<CsvUploadManageDataSource
|
||||
setAddDataSourceResult={setAddDataSourceResult}
|
||||
namespace={spaceId}
|
||||
/>
|
||||
)}
|
||||
{isIndexModalOpen && <IndexSelectorModal onClose={hideIndexModal} onImport={onDone} />}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -93,7 +93,7 @@ export const AddDataSourcePanel = ({ onComplete }: AddDataSourcePanelProps) => {
|
|||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.addDataSource.file.description"
|
||||
defaultMessage="Import a list of privileged users from a CSV, TXT, or TSV file"
|
||||
defaultMessage="Import a list of privileged users from a CSV file"
|
||||
/>
|
||||
}
|
||||
onClick={showImportFileModal}
|
||||
|
|
|
@ -29,21 +29,21 @@ import { useFetchPrivilegedUserIndices } from '../hooks/use_fetch_privileged_use
|
|||
import { useEntityAnalyticsRoutes } from '../../../api/api';
|
||||
import { CreateIndexModal } from './create_index_modal';
|
||||
|
||||
const SELECT_INDEX_LABEL = i18n.translate(
|
||||
export const SELECT_INDEX_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.selectIndex.comboboxPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select index',
|
||||
}
|
||||
);
|
||||
|
||||
const LOADING_ERROR_MESSAGE = i18n.translate(
|
||||
export const LOADING_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.selectIndex.error',
|
||||
{
|
||||
defaultMessage: 'Error loading indices. Please try again later.',
|
||||
}
|
||||
);
|
||||
|
||||
const DEBOUNCE_OPTIONS = { wait: 300 };
|
||||
export const DEBOUNCE_OPTIONS = { wait: 300 };
|
||||
|
||||
export const IndexSelectorModal = ({
|
||||
onClose,
|
||||
|
|
|
@ -36,6 +36,7 @@ import { useSourcererDataView } from '../../sourcerer/containers';
|
|||
import { HeaderPage } from '../../common/components/header_page';
|
||||
import { useEntityAnalyticsRoutes } from '../api/api';
|
||||
import { usePrivilegedMonitoringEngineStatus } from '../api/hooks/use_privileged_monitoring_engine_status';
|
||||
import { PrivilegedUserMonitoringManageDataSources } from '../components/privileged_user_monitoring_manage_data_sources';
|
||||
|
||||
type PageState =
|
||||
| { type: 'fetchingEngineStatus' }
|
||||
|
@ -45,7 +46,9 @@ type PageState =
|
|||
initResponse?: InitMonitoringEngineResponse | PrivMonHealthResponse;
|
||||
userCount: number;
|
||||
}
|
||||
| { type: 'dashboard'; onboardingCallout?: OnboardingCallout; error: string | undefined };
|
||||
| { type: 'dashboard'; onboardingCallout?: OnboardingCallout; error: string | undefined }
|
||||
| { type: 'initializingEngine'; initResponse?: InitMonitoringEngineResponse; userCount: number }
|
||||
| { type: 'manageDataSources' };
|
||||
|
||||
type Action =
|
||||
| { type: 'INITIALIZING_ENGINE'; userCount: number; initResponse?: InitMonitoringEngineResponse }
|
||||
|
@ -57,7 +60,8 @@ type Action =
|
|||
}
|
||||
| {
|
||||
type: 'SHOW_ONBOARDING';
|
||||
};
|
||||
}
|
||||
| { type: 'SHOW_MANAGE_DATA_SOURCES' };
|
||||
|
||||
const initialState: PageState = { type: 'fetchingEngineStatus' };
|
||||
function reducer(state: PageState, action: Action): PageState {
|
||||
|
@ -85,6 +89,8 @@ function reducer(state: PageState, action: Action): PageState {
|
|||
};
|
||||
}
|
||||
return state;
|
||||
case 'SHOW_MANAGE_DATA_SOURCES':
|
||||
return { type: 'manageDataSources' };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -117,7 +123,13 @@ export const EntityAnalyticsPrivilegedUserMonitoringPage = () => {
|
|||
[initPrivilegedMonitoringEngine]
|
||||
);
|
||||
|
||||
const onManageUserClicked = useCallback(() => {}, []);
|
||||
const onManageUserClicked = useCallback(() => {
|
||||
dispatch({ type: 'SHOW_MANAGE_DATA_SOURCES' });
|
||||
}, []);
|
||||
|
||||
const onBackToDashboardClicked = useCallback(() => {
|
||||
dispatch({ type: 'SHOW_DASHBOARD' });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (engineStatus.isLoading) {
|
||||
|
@ -262,7 +274,7 @@ export const EntityAnalyticsPrivilegedUserMonitoringPage = () => {
|
|||
<EuiButtonEmpty onClick={onManageUserClicked} iconType="gear" color="primary">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.dashboards.manageUsersButton"
|
||||
defaultMessage="Manage users"
|
||||
defaultMessage="Manage data sources"
|
||||
/>
|
||||
</EuiButtonEmpty>,
|
||||
]}
|
||||
|
@ -276,6 +288,13 @@ export const EntityAnalyticsPrivilegedUserMonitoringPage = () => {
|
|||
</>
|
||||
)}
|
||||
|
||||
{state.type === 'manageDataSources' && (
|
||||
<PrivilegedUserMonitoringManageDataSources
|
||||
onBackToDashboardClicked={onBackToDashboardClicked}
|
||||
onDone={initEngineCallBack}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SpyRoute pageName={SecurityPageName.entityAnalyticsPrivilegedUserMonitoring} />
|
||||
</SecuritySolutionPageWrapper>
|
||||
</>
|
||||
|
|
|
@ -37,6 +37,9 @@ export const PRIVILEGED_MONITOR_USERS_INDEX_MAPPING: MappingProperties = {
|
|||
'user.is_privileged': {
|
||||
type: 'boolean',
|
||||
},
|
||||
'labels.sources': {
|
||||
type: 'keyword',
|
||||
},
|
||||
};
|
||||
|
||||
export const generateUserIndexMappings = (): MappingTypeMapping => ({
|
||||
|
|
|
@ -18,11 +18,13 @@ export const bulkUpsertBatch =
|
|||
index,
|
||||
operations: users.flatMap((u) => {
|
||||
const id = batch.existingUsers[u.username];
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
if (!id) {
|
||||
return [
|
||||
{ create: {} },
|
||||
{
|
||||
'@timestamp': timestamp,
|
||||
user: { name: u.username, is_privileged: true },
|
||||
labels: { sources: ['csv'] },
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue