Added privileged users table to privileged user monitoring (#225084)

# Overview

Adds the initial privileged users table within the Privileged user
monitoring page.

Currently, this table shows:
- The user's risk score
- The user's asset criticality
- The data source that determined the privileged user
- The number of alerts associated with that privileged user in the
specified time range, along with its distribution


<img width="1310" alt="Screenshot 2025-06-24 at 3 41 17 PM"
src="https://github.com/user-attachments/assets/4093892d-896c-4ba9-a585-ad955f5661b7"
/>

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jared Burgett 2025-06-24 17:31:31 -05:00 committed by GitHub
parent 3f3c0025e1
commit 535c27fb90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 709 additions and 18 deletions

View file

@ -35,6 +35,7 @@ import type {
import type {
AssetCriticalityRecord,
EntityAnalyticsPrivileges,
FindAssetCriticalityRecordsResponse,
SearchPrivilegesIndicesResponse,
} from '../../../common/api/entity_analytics';
import {
@ -53,6 +54,7 @@ import {
RISK_ENGINE_CLEANUP_URL,
RISK_ENGINE_SCHEDULE_NOW_URL,
RISK_ENGINE_CONFIGURE_SO_URL,
ASSET_CRITICALITY_PUBLIC_LIST_URL,
} from '../../../common/constants';
import type { SnakeToCamelCase } from '../common/utils';
import { useKibana } from '../../common/lib/kibana/kibana_react';
@ -300,6 +302,26 @@ export const useEntityAnalyticsRoutes = () => {
});
};
/**
* Get multiple asset criticality records
*/
const fetchAssetCriticalityList = async (params: {
idField: string;
idValues: string[];
}): Promise<FindAssetCriticalityRecordsResponse> => {
const wrapWithQuotes = (each: string) => `"${each}"`;
const kueryValues = `${params.idValues.map(wrapWithQuotes).join(' OR ')}`;
const kuery = `${params.idField}: (${kueryValues})`;
return http.fetch<FindAssetCriticalityRecordsResponse>(ASSET_CRITICALITY_PUBLIC_LIST_URL, {
version: API_VERSIONS.public.v1,
method: 'GET',
query: {
kuery,
},
});
};
const uploadAssetCriticalityFile = async (
fileContent: string,
fileName: string
@ -397,6 +419,7 @@ export const useEntityAnalyticsRoutes = () => {
createAssetCriticality,
deleteAssetCriticality,
fetchAssetCriticality,
fetchAssetCriticalityList,
uploadAssetCriticalityFile,
uploadPrivilegedUserMonitoringFile,
initPrivilegedMonitoringEngine,

View file

@ -67,6 +67,9 @@ export enum HostRiskScoreQueryId {
export const formatRiskScore = (riskScore: number) =>
(Math.round(riskScore * 100) / 100).toFixed(2);
export const formatRiskScoreWholeNumber = (riskScore: number) =>
(Math.round(riskScore * 100) / 100).toFixed(0);
export const FIRST_RECORD_PAGINATION = {
cursorStart: 0,
querySize: 1,

View file

@ -10,7 +10,10 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import type { SecurityAppError } from '@kbn/securitysolution-t-grid';
import type { EntityType } from '../../../../common/entity_analytics/types';
import { EntityTypeToIdentifierField } from '../../../../common/entity_analytics/types';
import type { EntityAnalyticsPrivileges } from '../../../../common/api/entity_analytics';
import type {
EntityAnalyticsPrivileges,
FindAssetCriticalityRecordsResponse,
} from '../../../../common/api/entity_analytics';
import type { CriticalityLevelWithUnassigned } from '../../../../common/entity_analytics/asset_criticality/types';
import { useHasSecurityCapability } from '../../../helper_hooks';
import type { AssetCriticalityRecord } from '../../../../common/api/entity_analytics/asset_criticality';
@ -18,6 +21,7 @@ import type { AssetCriticality, DeleteAssetCriticalityResponse } from '../../api
import { useEntityAnalyticsRoutes } from '../../api/api';
const ASSET_CRITICALITY_KEY = 'ASSET_CRITICALITY';
const ASSET_CRITICALITY_LIST_KEY = 'ASSET_CRITICALITY_LIST';
const PRIVILEGES_KEY = 'PRIVILEGES';
const nonAuthorizedResponse: Promise<EntityAnalyticsPrivileges> = Promise.resolve({
@ -43,6 +47,21 @@ export const useAssetCriticalityPrivileges = (
});
};
export const useAssetCriticalityFetchList = ({
idField,
idValues,
}: {
idField: string;
idValues: string[];
}) => {
const { fetchAssetCriticalityList } = useEntityAnalyticsRoutes();
return useQuery<FindAssetCriticalityRecordsResponse>({
queryKey: [ASSET_CRITICALITY_LIST_KEY],
queryFn: () => fetchAssetCriticalityList({ idField, idValues }),
enabled: idValues.length > 0,
});
};
export const useAssetCriticalityData = ({
entity,
enabled = true,

View file

@ -0,0 +1,328 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n-react';
import { isArray } from 'lodash/fp';
import React from 'react';
import { i18n } from '@kbn/i18n';
import type { EuiBasicTableColumn, EuiThemeComputed } from '@elastic/eui';
import {
EuiLink,
EuiFlexItem,
useEuiTheme,
EuiBadge,
EuiText,
EuiLoadingSpinner,
EuiButtonIcon,
EuiFlexGroup,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { SecurityPageName, useNavigation } from '@kbn/security-solution-navigation';
import { encode } from '@kbn/rison';
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
import { ALERTS_QUERY_NAMES } from '../../../../../detections/containers/detection_engine/alerts/constants';
import type { AlertsByStatusAgg } from '../../../../../overview/components/detection_response/alerts_by_status/types';
import { getRowItemsWithActions } from '../../../../../common/components/tables/helpers';
import { UserName } from '../../../user_name';
import { getEmptyTagValue } from '../../../../../common/components/empty_value';
import type { TableItemType } from './types';
import { formatRiskScoreWholeNumber } from '../../../../common/utils';
import { AssetCriticalityBadge } from '../../../asset_criticality';
import { useSignalIndex } from '../../../../../detections/containers/detection_engine/alerts/use_signal_index';
import {
getAlertsByStatusQuery,
parseAlertsData,
} from '../../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
import { useGlobalTime } from '../../../../../common/containers/use_global_time';
import { useQueryAlerts } from '../../../../../detections/containers/detection_engine/alerts/use_query';
import { formatPageFilterSearchParam } from '../../../../../../common/utils/format_page_filter_search_param';
import { URL_PARAM_KEY } from '../../../../../common/hooks/constants';
import {
OPEN_IN_ALERTS_TITLE_STATUS,
OPEN_IN_ALERTS_TITLE_USERNAME,
} from '../../../../../overview/components/detection_response/translations';
import { FILTER_ACKNOWLEDGED, FILTER_OPEN } from '../../../../../../common/types';
import type { CriticalityLevelWithUnassigned } from '../../../../../../common/entity_analytics/asset_criticality/types';
import { getFormattedAlertStats } from '../../../../../flyout/document_details/shared/components/alert_count_insight';
const COLUMN_WIDTHS = { actions: '5%', '@timestamp': '20%', privileged_user: '15%' };
const getPrivilegedUserColumn = (fieldName: string) => ({
field: 'user.name',
name: (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.columns.privilegedUser"
defaultMessage="Privileged user"
/>
),
width: COLUMN_WIDTHS.privileged_user,
render: (user: string[] | string) =>
user != null
? getRowItemsWithActions({
values: isArray(user) ? user : [user],
fieldName,
idPrefix: 'privileged-user-monitoring-privileged-user',
render: (item) => <UserName userName={item} />,
displayCount: 1,
})
: getEmptyTagValue(),
});
const getActionsColumn = (openUserFlyout: (userName: string) => void) => ({
name: (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.columns.actions"
defaultMessage="Actions"
/>
),
render: (record: { 'user.name': string }) => {
return (
<EuiButtonIcon
iconType="expand"
onClick={() => {
openUserFlyout(record['user.name']);
}}
aria-label={i18n.translate(
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.columns.expand.ariaLabel',
{
defaultMessage: 'Open user flyout',
}
)}
/>
);
},
width: COLUMN_WIDTHS.actions,
});
const getRiskScoreColumn = (euiTheme: EuiThemeComputed) => ({
name: (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.columns.riskScore"
defaultMessage="Risk score"
/>
),
render: (record: TableItemType) => {
const colors: { background: string; text: string } = (() => {
switch (record.risk_level) {
case 'Unknown':
return {
background: euiTheme.colors.backgroundBaseSubdued,
text: euiTheme.colors.textSubdued,
};
case 'Low':
return {
background: euiTheme.colors.backgroundBaseNeutral,
text: euiTheme.colors.textNeutral,
};
case 'Moderate':
return {
background: euiTheme.colors.backgroundLightWarning,
text: euiTheme.colors.textWarning,
};
case 'High':
return {
background: euiTheme.colors.backgroundLightRisk,
text: euiTheme.colors.textRisk,
};
case 'Critical':
return {
background: euiTheme.colors.backgroundLightDanger,
text: euiTheme.colors.textDanger,
};
default:
return {
background: euiTheme.colors.backgroundBaseSubdued,
text: euiTheme.colors.textSubdued,
};
}
})();
return (
<EuiBadge color={colors.background}>
<EuiText
css={css`
font-weight: ${euiTheme.font.weight.semiBold};
`}
size={'s'}
color={colors.text}
>
{record.risk_score
? formatRiskScoreWholeNumber(record.risk_score)
: i18n.translate(
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.columns.riskScore.na',
{ defaultMessage: 'N/A' }
)}
</EuiText>
</EuiBadge>
);
},
});
const AssetCriticalityCell: React.FC<{
criticalityLevel?: CriticalityLevelWithUnassigned;
}> = ({ criticalityLevel }) => {
return criticalityLevel ? (
<AssetCriticalityBadge
criticalityLevel={criticalityLevel}
dataTestSubj="privileged-user-monitoring-asset-criticality-badge"
/>
) : (
getEmptyTagValue()
);
};
const getAssetCriticalityColumn = () => ({
name: (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.columns.assetCriticality"
defaultMessage="Asset Criticality"
/>
),
render: (record: TableItemType) => (
<AssetCriticalityCell criticalityLevel={record.criticality_level} />
),
});
function dataSourcesIsArray(dataSources: string | string[]): dataSources is string[] {
return Array.isArray(dataSources);
}
const prettyDataSource = (dataSource: string) => {
return (() => {
switch (dataSource) {
case 'csv':
return i18n.translate(
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.columns.dataSource.csv',
{ defaultMessage: 'CSV File' }
);
case 'index':
return i18n.translate(
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.columns.dataSource.index',
{ defaultMessage: 'Index' }
);
case 'api':
return i18n.translate(
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.columns.dataSource.api',
{ defaultMessage: 'API request' }
);
default:
return dataSource;
}
})();
};
const getDataSourceColumn = () => ({
name: (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.columns.dataSource"
defaultMessage="Data source"
/>
),
render: (record: TableItemType) => {
const dataSources = record['labels.sources'];
if (!dataSources) return getEmptyTagValue();
if (dataSourcesIsArray(dataSources))
return dataSources.map((source) => prettyDataSource(source)).join(', ');
return prettyDataSource(dataSources);
},
});
const PrivilegedUserAlertDistribution: React.FC<{ userName: string }> = ({ userName }) => {
const { signalIndexName } = useSignalIndex();
const { from, to } = useGlobalTime();
const { euiTheme } = useEuiTheme();
const { data } = useQueryAlerts<{}, AlertsByStatusAgg>({
query: getAlertsByStatusQuery({
from,
to,
entityFilter: { field: 'user.name', value: userName },
}),
indexName: signalIndexName,
queryName: ALERTS_QUERY_NAMES.BY_STATUS,
});
const { navigateTo } = useNavigation();
if (!data) return <EuiLoadingSpinner size="m" />;
const alertsData = parseAlertsData(data);
const alertStats = getFormattedAlertStats(alertsData, euiTheme);
const alertCount = (alertsData?.open?.total ?? 0) + (alertsData?.acknowledged?.total ?? 0);
const timerange = encode({
global: {
[URL_PARAM_KEY.timerange]: {
kind: 'absolute',
from,
to,
},
},
});
const filters = [
{
title: OPEN_IN_ALERTS_TITLE_USERNAME,
selectedOptions: [userName],
fieldName: 'user.name',
},
{
title: OPEN_IN_ALERTS_TITLE_STATUS,
selectedOptions: [FILTER_OPEN, FILTER_ACKNOWLEDGED],
fieldName: 'kibana.alert.workflow_status',
},
];
const urlFilterParams = encode(formatPageFilterSearchParam(filters));
const timerangePath = timerange ? `&timerange=${timerange}` : '';
const openAlertsPage = () => {
navigateTo({
deepLinkId: SecurityPageName.alerts,
path: `?${URL_PARAM_KEY.pageFilter}=${urlFilterParams}${timerangePath}`,
openInNewTab: true,
});
};
return (
<EuiFlexGroup direction="row">
<EuiFlexItem>
<DistributionBar
stats={alertStats}
hideLastTooltip
data-test-subj={`privileged-users-alerts-distribution-bar`}
/>
</EuiFlexItem>
<EuiBadge color="hollow">{<EuiLink onClick={openAlertsPage}>{alertCount}</EuiLink>}</EuiBadge>
</EuiFlexGroup>
);
};
const getAlertDistributionColumn = () => ({
name: (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.columns.alerts"
defaultMessage="Alerts"
/>
),
render: (record: TableItemType) => {
return <PrivilegedUserAlertDistribution userName={record['user.name']} />;
},
});
export const buildPrivilegedUsersTableColumns = (
openUserFlyout: (userName: string) => void,
euiTheme: EuiThemeComputed
): Array<EuiBasicTableColumn<TableItemType>> => [
getActionsColumn(openUserFlyout),
getPrivilegedUserColumn('user.name'),
getRiskScoreColumn(euiTheme),
getAssetCriticalityColumn(),
getDataSourceColumn(),
getAlertDistributionColumn(),
];

View file

@ -0,0 +1,16 @@
/*
* 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 { getPrivilegedMonitorUsersIndex } from '../../../../../../common/entity_analytics/privilege_monitoring/constants';
import { getPrivilegedMonitorUsersJoin } from '../../queries/helpers';
export const getPrivilegedUsersQuery = (namespace: string) => {
return `FROM ${getPrivilegedMonitorUsersIndex(namespace)}
${getPrivilegedMonitorUsersJoin(namespace)}
| STATS user.is_privileged = TOP(user.is_privileged, 1, "desc"), labels.sources = TOP(labels.sources, 1, "desc") BY user.name
`;
};

View file

@ -0,0 +1,283 @@
/*
* 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, useState } from 'react';
import {
useEuiTheme,
EuiPanel,
EuiFlexGroup,
EuiText,
EuiFlexItem,
EuiSpacer,
EuiHorizontalRule,
EuiButtonEmpty,
EuiBasicTable,
EuiProgress,
EuiCallOut,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { take } from 'lodash/fp';
import { FormattedMessage } from '@kbn/i18n-react';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useQuery } from '@tanstack/react-query';
import { getESQLResults } from '@kbn/esql-utils';
import { i18n } from '@kbn/i18n';
import { useQueryToggle } from '../../../../../common/containers/query_toggle';
import { useRiskScore } from '../../../../api/hooks/use_risk_score';
import type { TableItemType } from './types';
import { getPrivilegedUsersQuery } from './esql_source_query';
import { UserPanelKey } from '../../../../../flyout/entity_details/shared/constants';
import type { EntityRiskScore } from '../../../../../../common/search_strategy';
import { buildEntityNameFilter, EntityType } from '../../../../../../common/search_strategy';
import { buildPrivilegedUsersTableColumns } from './columns';
import { esqlResponseToRecords } from '../../../../../common/utils/esql';
import { useKibana } from '../../../../../common/lib/kibana';
import { useGlobalFilterQuery } from '../../../../../common/hooks/use_global_filter_query';
import { HeaderSection } from '../../../../../common/components/header_section';
import { useAssetCriticalityFetchList } from '../../../asset_criticality/use_asset_criticality';
import type { CriticalityLevelWithUnassigned } from '../../../../../../common/entity_analytics/asset_criticality/types';
export const DEFAULT_PAGE_SIZE = 10;
export const PRIVILEGED_USERS_TABLE_QUERY_ID = 'privmonPrivilegedUsersTableQueryId';
const TITLE = i18n.translate(
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.title',
{ defaultMessage: 'Privileged users' }
);
const PRIVILEGED_USERS_TABLE_ID = 'PrivilegedUsers-table';
const useOpenUserFlyout = () => {
const { openFlyout } = useExpandableFlyoutApi();
return (userName: string) => {
openFlyout({
right: {
id: UserPanelKey,
params: {
contextID: PRIVILEGED_USERS_TABLE_ID,
userName,
scopeId: PRIVILEGED_USERS_TABLE_ID,
},
},
});
};
};
interface RiskScoresByUserName {
[key: string]: EntityRiskScore<EntityType.user>;
}
interface AssetCriticalityByUserName {
[key: string]: CriticalityLevelWithUnassigned;
}
export const PrivilegedUsersTable: React.FC<{ spaceId: string }> = ({ spaceId }) => {
const { toggleStatus, setToggleStatus } = useQueryToggle(PRIVILEGED_USERS_TABLE_QUERY_ID);
const { euiTheme } = useEuiTheme();
const { data } = useKibana().services;
const openUserFlyout = useOpenUserFlyout();
const columns = buildPrivilegedUsersTableColumns(openUserFlyout, euiTheme);
const [currentPage, setCurrentPage] = useState<number>(1);
const privilegedUsersTableQuery = getPrivilegedUsersQuery(spaceId);
const { filterQuery: filterQueryWithoutTimerange } = useGlobalFilterQuery();
const {
data: result,
isLoading: loadingPrivilegedUsers,
isError: privilegedUsersError,
} = useQuery({
queryKey: ['privileged-users-table', privilegedUsersTableQuery, filterQueryWithoutTimerange],
queryFn: async () => {
return getESQLResults({
esqlQuery: privilegedUsersTableQuery,
search: data.search.search,
filter: filterQueryWithoutTimerange,
});
},
});
const records = useMemo(() => esqlResponseToRecords<TableItemType>(result?.response), [result]);
const nameFilterQuery = useMemo(() => {
const userNames = records.map((user) => user['user.name']);
return buildEntityNameFilter(EntityType.user, userNames);
}, [records]);
const {
data: riskScoreData,
error: riskScoreError,
loading: loadingRiskScore,
hasEngineBeenInstalled: hasRiskEngineBeenInstalled,
} = useRiskScore<EntityType.user>({
riskEntity: EntityType.user,
filterQuery: nameFilterQuery,
onlyLatest: true,
pagination: {
cursorStart: 0,
querySize: records.length,
},
skip: nameFilterQuery === undefined,
});
const riskScores = riskScoreData && riskScoreData.length > 0 ? riskScoreData : [];
const riskScoreByUserName: RiskScoresByUserName = Object.fromEntries(
riskScores.map((riskScore) => [riskScore.user.name, riskScore])
);
const {
data: assetCriticalityData,
isError: assetCriticalityError,
isLoading: loadingAssetCriticality,
} = useAssetCriticalityFetchList({
idField: 'user.name',
idValues: records.map((user) => user['user.name']),
});
const assetCriticalityRecords =
assetCriticalityData && assetCriticalityData.records.length > 0
? assetCriticalityData.records
: [];
const assetCriticalityByUserName: AssetCriticalityByUserName = Object.fromEntries(
assetCriticalityRecords.map((assetCriticalityRecord) => [
assetCriticalityRecord.id_value,
assetCriticalityRecord.criticality_level,
])
);
const enrichedRecords: TableItemType[] = useMemo(
() =>
records.map((record, index) => {
let enrichedFields = {};
const riskScore: EntityRiskScore<EntityType.user> | undefined =
riskScoreByUserName[record['user.name']];
if (riskScore) {
enrichedFields = {
...enrichedFields,
risk_score: riskScore.user.risk.calculated_score_norm,
risk_level: riskScore.user.risk.calculated_level,
};
}
const assetCriticality: CriticalityLevelWithUnassigned | undefined =
assetCriticalityByUserName[record['user.name']];
if (assetCriticality) {
enrichedFields = {
...enrichedFields,
criticality_level: assetCriticality,
};
}
return {
...record,
...enrichedFields,
};
}),
[records, riskScoreByUserName, assetCriticalityByUserName]
);
const visibleRecords = take(currentPage * DEFAULT_PAGE_SIZE, enrichedRecords);
return (
<EuiPanel hasBorder hasShadow={false} data-test-subj="privileged-users-table-panel">
<HeaderSection
toggleStatus={toggleStatus}
toggleQuery={setToggleStatus}
id={PRIVILEGED_USERS_TABLE_QUERY_ID}
showInspectButton={false}
title={TITLE}
titleSize="s"
outerDirection="column"
hideSubtitle
/>
{(privilegedUsersError ||
(hasRiskEngineBeenInstalled && riskScoreError) ||
assetCriticalityError) && (
<EuiCallOut
title={i18n.translate(
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.error',
{
defaultMessage:
'There was an error retrieving privileged users. Results may be incomplete.',
}
)}
color="danger"
iconType="error"
/>
)}
{toggleStatus && (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
{(loadingPrivilegedUsers || loadingRiskScore || loadingAssetCriticality) && (
<EuiProgress size="xs" color="accent" />
)}
</EuiFlexItem>
{records.length > 0 && (
<>
<EuiText size={'s'}>
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.showing"
defaultMessage="Showing "
/>
<span
css={css`
font-weight: ${euiTheme.font.weight.bold};
`}
>
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.privilegedUsersTable.countOfUsers"
defaultMessage="{count} privileged {count, plural, one {user} other {users}}"
values={{ count: visibleRecords.length }}
/>
</span>
</EuiText>
<EuiSpacer size="s" />
<EuiHorizontalRule margin="none" style={{ height: 2 }} />
<EuiBasicTable
id={PRIVILEGED_USERS_TABLE_QUERY_ID}
loading={loadingPrivilegedUsers || loadingRiskScore || loadingAssetCriticality}
items={visibleRecords || []}
columns={columns}
/>
</>
)}
{records.length > currentPage * DEFAULT_PAGE_SIZE && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
isLoading={loadingRiskScore || loadingPrivilegedUsers || loadingAssetCriticality}
onClick={() => {
setCurrentPage((page) => page + 1);
}}
flush="right"
color="primary"
size="s"
iconType="sortDown"
iconSide="right"
iconSize="s"
>
<FormattedMessage
id="xpack.securitySolution.privilegedUserMonitoring.showMore"
defaultMessage="Show more"
/>
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</EuiPanel>
);
};

View file

@ -0,0 +1,17 @@
/*
* 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 { CriticalityLevelWithUnassigned } from '../../../../../../common/entity_analytics/asset_criticality/types';
import type { EntityRiskLevels } from '../../../../../../common/api/entity_analytics/common';
export interface TableItemType extends Record<string, string | string[] | number | undefined> {
'user.name': string;
'labels.sources'?: string | string[];
risk_score?: number;
risk_level?: EntityRiskLevels;
criticality_level?: CriticalityLevelWithUnassigned;
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DataViewSpec } from '@kbn/data-views-plugin/public';
@ -14,6 +14,7 @@ import { RiskLevelsPrivilegedUsersPanel } from './components/risk_level_panel';
import { KeyInsightsPanel } from './components/key_insights_panel';
import { UserActivityPrivilegedUsersPanel } from './components/privileged_user_activity';
import { PrivilegedAccessDetectionsPanel } from './components/privileged_access_detection';
import { PrivilegedUsersTable } from './components/privileged_users_table';
export interface OnboardingCallout {
userCount: number;
@ -102,15 +103,11 @@ export const PrivilegedUserMonitoring = ({
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{spaceId && <PrivilegedUsersTable spaceId={spaceId} />}
{spaceId && <PrivilegedAccessDetectionsPanel spaceId={spaceId} />}
<EuiFlexItem>
<UserActivityPrivilegedUsersPanel sourcererDataView={sourcererDataView} />
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel hasShadow={false} hasBorder={true}>
{'TODO: Privileged users'}
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -17,20 +17,25 @@ const getLatestCSVPrivilegedUserUploadQuery = (namespace: string) => {
| STATS latest_timestamp = MAX(@timestamp)`;
};
const GET_LATEST_CSV_UPLOAD_QUERY_ID = 'getPrivilegedUserMonitoringLatestCsvQuery';
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 { isLoading, data, isError, refetch } = useQuery(
[GET_LATEST_CSV_UPLOAD_QUERY_ID],
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,