mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
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:
parent
3f3c0025e1
commit
535c27fb90
9 changed files with 709 additions and 18 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
];
|
|
@ -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
|
||||
`;
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue