mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[SecuritySolution] Bootstrap Privileged User Monitoring page (#217350)
## Summary Bootstrap Privileged User Monitoring page. This page is hidden behind `privilegeMonitoringEnabled` flag.  ### Included * Add the Privileged User Monitoring page content according to design * Link integrations to the integrations page * Find index modal * New API to search for compatible indices * It also renames the navigation title to only have the first letter capitalised. ### Not Included * The navigation is already implemented by https://github.com/elastic/kibana/pull/217180 * The video introduction * The final API call in the "choose index" is out of scope for this issue. * The CSV upload functionality is entirely out of scope for this ticket. * The "Sample Dashboard" * The link to docs ### How to test it? * Enable `privilegeMonitoringEnabled` flag. * Start kibana. * Use the menu to navigate to the Priv User monitoring page ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
cc18709d01
commit
b623001080
47 changed files with 1272 additions and 72 deletions
|
@ -84,7 +84,7 @@ export enum SecurityPageName {
|
|||
entityAnalyticsManagement = 'entity_analytics-management',
|
||||
entityAnalyticsAssetClassification = 'entity_analytics-asset-classification',
|
||||
entityAnalyticsLanding = 'entity_analytics-landing',
|
||||
privilegedUserMonitoring = 'entity_analytics-privileged_user_monitoring',
|
||||
entityAnalyticsPrivilegedUserMonitoring = 'entity_analytics-privileged_user_monitoring',
|
||||
entityAnalyticsEntityStoreManagement = 'entity_analytics-entity_store_management',
|
||||
coverageOverview = 'coverage-overview',
|
||||
notes = 'notes',
|
||||
|
|
|
@ -141,6 +141,7 @@ export const useGetPackageInfoByKeyQuery = (
|
|||
queryOptions: {
|
||||
// If enabled is false, the query will not be fetched
|
||||
enabled?: boolean;
|
||||
suspense?: boolean;
|
||||
refetchOnMount?: boolean | 'always';
|
||||
} = {
|
||||
enabled: true,
|
||||
|
@ -164,6 +165,7 @@ export const useGetPackageInfoByKeyQuery = (
|
|||
},
|
||||
}),
|
||||
{
|
||||
suspense: queryOptions.suspense,
|
||||
enabled: queryOptions.enabled,
|
||||
refetchOnMount: queryOptions.refetchOnMount,
|
||||
retry: (_, error) => !isRegistryConnectionError(error),
|
||||
|
|
|
@ -91,7 +91,13 @@ export const AvailablePackagesHook = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const LazyPackageCard = lazy(() =>
|
||||
import('./applications/integrations/sections/epm/components/package_card').then((module) => ({
|
||||
default: module.PackageCard,
|
||||
}))
|
||||
);
|
||||
|
||||
export { useGetDataStreams } from './hooks/use_request/data_stream';
|
||||
export { useGetPackagesQuery } from './hooks/use_request/epm';
|
||||
export { useGetPackagesQuery, useGetPackageInfoByKeyQuery } from './hooks/use_request/epm';
|
||||
export { useGetSettingsQuery } from './hooks/use_request/settings';
|
||||
export { useLink } from './hooks/use_link';
|
||||
|
|
|
@ -148,14 +148,14 @@ export const i18nStrings = {
|
|||
},
|
||||
},
|
||||
entityRiskScore: i18n.translate('securitySolutionPackages.navLinks.entityRiskScore', {
|
||||
defaultMessage: 'Entity Risk Score',
|
||||
defaultMessage: 'Entity risk score',
|
||||
}),
|
||||
entityStore: i18n.translate('securitySolutionPackages.navLinks.entityStore', {
|
||||
defaultMessage: 'Entity Store',
|
||||
defaultMessage: 'Entity store',
|
||||
}),
|
||||
entityAnalytics: {
|
||||
landing: i18n.translate('securitySolutionPackages.navLinks.entityAnalytics', {
|
||||
defaultMessage: 'Entity Analytics',
|
||||
defaultMessage: 'Entity analytics',
|
||||
}),
|
||||
},
|
||||
devTools: i18n.translate('securitySolutionPackages.navLinks.devTools', {
|
||||
|
|
|
@ -25,7 +25,7 @@ export const ENTITY_ANALYTICS_LICENSE_DESC = i18n.translate(
|
|||
export const ENTITY_ANALYTICS_TITLE = i18n.translate(
|
||||
'securitySolutionPackages.entityAnalytics.navigation',
|
||||
{
|
||||
defaultMessage: 'Entity Analytics',
|
||||
defaultMessage: 'Entity analytics',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -8,4 +8,6 @@
|
|||
export * from './asset_criticality';
|
||||
export * from './risk_engine';
|
||||
export * from './entity_store';
|
||||
export * from './monitoring';
|
||||
|
||||
export type { EntityAnalyticsPrivileges } from './common';
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './search_indices.gen';
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Search indices for Privileges Monitoring import
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
|
||||
export type SearchPrivilegesIndicesRequestQuery = z.infer<
|
||||
typeof SearchPrivilegesIndicesRequestQuery
|
||||
>;
|
||||
export const SearchPrivilegesIndicesRequestQuery = z.object({
|
||||
searchQuery: z.string().optional(),
|
||||
});
|
||||
export type SearchPrivilegesIndicesRequestQueryInput = z.input<
|
||||
typeof SearchPrivilegesIndicesRequestQuery
|
||||
>;
|
||||
|
||||
export type SearchPrivilegesIndicesResponse = z.infer<typeof SearchPrivilegesIndicesResponse>;
|
||||
export const SearchPrivilegesIndicesResponse = z.array(z.string());
|
|
@ -0,0 +1,28 @@
|
|||
openapi: 3.0.0
|
||||
|
||||
info:
|
||||
title: Search indices for Privileges Monitoring import
|
||||
version: '2023-10-31'
|
||||
paths:
|
||||
/api/entity_analytics/monitoring/privileges/indices:
|
||||
get:
|
||||
x-labels: [ess, serverless]
|
||||
x-internal: true
|
||||
x-codegen-enabled: true
|
||||
operationId: SearchPrivilegesIndices
|
||||
summary: Search Indices for Privileges Monitoring import
|
||||
parameters:
|
||||
- name: searchQuery
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
description: The search query to filter the indices
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
|
@ -258,6 +258,10 @@ import type {
|
|||
GetEntityStoreStatusRequestQueryInput,
|
||||
GetEntityStoreStatusResponse,
|
||||
} from './entity_analytics/entity_store/status.gen';
|
||||
import type {
|
||||
SearchPrivilegesIndicesRequestQueryInput,
|
||||
SearchPrivilegesIndicesResponse,
|
||||
} from './entity_analytics/monitoring/search_indices.gen';
|
||||
import type { InitMonitoringEngineResponse } from './entity_analytics/privilege_monitoring/engine/init.gen';
|
||||
import type { PrivMonHealthResponse } from './entity_analytics/privilege_monitoring/health.gen';
|
||||
import type { CleanUpRiskEngineResponse } from './entity_analytics/risk_engine/engine_cleanup_route.gen';
|
||||
|
@ -2138,6 +2142,20 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
|
|||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
async searchPrivilegesIndices(props: SearchPrivilegesIndicesProps) {
|
||||
this.log.info(`${new Date().toISOString()} Calling API SearchPrivilegesIndices`);
|
||||
return this.kbnClient
|
||||
.request<SearchPrivilegesIndicesResponse>({
|
||||
path: '/api/entity_analytics/monitoring/privileges/indices',
|
||||
headers: {
|
||||
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
|
||||
},
|
||||
method: 'GET',
|
||||
|
||||
query: props.query,
|
||||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
/**
|
||||
* Assign users to detection alerts, and unassign them from alerts.
|
||||
> info
|
||||
|
@ -2612,6 +2630,9 @@ export interface RunScriptActionProps {
|
|||
export interface SearchAlertsProps {
|
||||
body: SearchAlertsRequestBodyInput;
|
||||
}
|
||||
export interface SearchPrivilegesIndicesProps {
|
||||
query: SearchPrivilegesIndicesRequestQueryInput;
|
||||
}
|
||||
export interface SetAlertAssigneesProps {
|
||||
body: SetAlertAssigneesRequestBodyInput;
|
||||
}
|
||||
|
|
|
@ -21,12 +21,12 @@ export const DATA_QUALITY = i18n.translate(
|
|||
export const ENTITY_ANALYTICS_RISK_SCORE = i18n.translate(
|
||||
'xpack.securitySolution.navigation.entityRiskScore',
|
||||
{
|
||||
defaultMessage: 'Entity Risk Score',
|
||||
defaultMessage: 'Entity risk score',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENTITY_STORE = i18n.translate('xpack.securitySolution.navigation.entityStore', {
|
||||
defaultMessage: 'Entity Store',
|
||||
defaultMessage: 'Entity store',
|
||||
});
|
||||
|
||||
export const NOTES = i18n.translate('xpack.securitySolution.navigation.notes', {
|
||||
|
@ -50,14 +50,14 @@ export const DETECTION_RESPONSE = i18n.translate(
|
|||
export const ENTITY_ANALYTICS = i18n.translate(
|
||||
'xpack.securitySolution.navigation.entityAnalytics',
|
||||
{
|
||||
defaultMessage: 'Entity Analytics',
|
||||
defaultMessage: 'Entity analytics',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING = i18n.translate(
|
||||
'xpack.securitySolution.navigation.privilegedUserMonitoring',
|
||||
{
|
||||
defaultMessage: 'Privileged User Monitoring',
|
||||
defaultMessage: 'Privileged user monitoring',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import type { GetAppUrl } from '@kbn/security-solution-navigation/src/navigation';
|
||||
import { useNavigation } from '@kbn/security-solution-navigation/src/navigation';
|
||||
import { APP_UI_ID } from '../../../../common';
|
||||
|
||||
export const useIntegrationLinkState = (path: string) => {
|
||||
const { getAppUrl } = useNavigation();
|
||||
|
||||
return useMemo(() => getIntegrationLinkState(path, getAppUrl), [getAppUrl, path]);
|
||||
};
|
||||
|
||||
export const getIntegrationLinkState = (path: string, getAppUrl: GetAppUrl) => {
|
||||
const url = getAppUrl({
|
||||
appId: APP_UI_ID,
|
||||
path,
|
||||
});
|
||||
|
||||
return {
|
||||
onCancelNavigateTo: [APP_UI_ID, { path }],
|
||||
onCancelUrl: url,
|
||||
onSaveNavigateTo: [APP_UI_ID, { path }],
|
||||
};
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { addPathParamToUrl, RETURN_APP_ID, RETURN_PATH } from '.';
|
||||
import { APP_UI_ID } from '../../../../common';
|
||||
|
||||
describe('addPathParamToUrl', () => {
|
||||
const encodedOnboardingLink = encodeURIComponent('/onboarding');
|
||||
it('should append query parameters to a URL without existing query parameters', () => {
|
||||
const url = 'https://example.com';
|
||||
const onboardingLink = '/onboarding';
|
||||
const result = addPathParamToUrl(url, onboardingLink);
|
||||
|
||||
expect(result).toBe(
|
||||
`https://example.com?${RETURN_APP_ID}=${APP_UI_ID}&${RETURN_PATH}=${encodedOnboardingLink}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should append query parameters to a URL with existing query parameters', () => {
|
||||
const url = 'https://example.com?foo=bar';
|
||||
const onboardingLink = '/onboarding';
|
||||
const result = addPathParamToUrl(url, onboardingLink);
|
||||
|
||||
expect(result).toBe(
|
||||
`https://example.com?foo=bar&${RETURN_APP_ID}=${APP_UI_ID}&${RETURN_PATH}=${encodedOnboardingLink}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode the onboarding link correctly', () => {
|
||||
const url = 'https://example.com';
|
||||
const onboardingLink = '/onboarding?step=1&next=2';
|
||||
const customEncodedOnboardingLink = encodeURIComponent(onboardingLink);
|
||||
const result = addPathParamToUrl(url, onboardingLink);
|
||||
|
||||
expect(result).toBe(
|
||||
`https://example.com?${RETURN_APP_ID}=${APP_UI_ID}&${RETURN_PATH}=${customEncodedOnboardingLink}`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { stringifyUrl } from 'query-string';
|
||||
import { APP_UI_ID } from '../../../../common';
|
||||
|
||||
export const RETURN_APP_ID = 'returnAppId';
|
||||
export const RETURN_PATH = 'returnPath';
|
||||
|
||||
export const addPathParamToUrl = (url: string, onboardingLink: string): string => {
|
||||
return stringifyUrl(
|
||||
{
|
||||
url,
|
||||
query: {
|
||||
[RETURN_APP_ID]: APP_UI_ID,
|
||||
[RETURN_PATH]: onboardingLink,
|
||||
},
|
||||
},
|
||||
{
|
||||
encode: true,
|
||||
sort: false,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -27,6 +27,7 @@ import type {
|
|||
import type {
|
||||
AssetCriticalityRecord,
|
||||
EntityAnalyticsPrivileges,
|
||||
SearchPrivilegesIndicesResponse,
|
||||
} from '../../../common/api/entity_analytics';
|
||||
import {
|
||||
RISK_ENGINE_STATUS_URL,
|
||||
|
@ -183,6 +184,25 @@ export const useEntityAnalyticsRoutes = () => {
|
|||
method: 'GET',
|
||||
});
|
||||
|
||||
/**
|
||||
* Search indices for privilege monitoring import
|
||||
*/
|
||||
const searchPrivMonIndices = async (params: {
|
||||
query: string | undefined;
|
||||
signal?: AbortSignal;
|
||||
}) =>
|
||||
http.fetch<SearchPrivilegesIndicesResponse>(
|
||||
'/api/entity_analytics/monitoring/privileges/indices',
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
method: 'GET',
|
||||
query: {
|
||||
searchQuery: params.query,
|
||||
},
|
||||
signal: params.signal,
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Create asset criticality
|
||||
*/
|
||||
|
@ -294,6 +314,7 @@ export const useEntityAnalyticsRoutes = () => {
|
|||
fetchRiskEnginePrivileges,
|
||||
fetchAssetCriticalityPrivileges,
|
||||
fetchEntityStorePrivileges,
|
||||
searchPrivMonIndices,
|
||||
createAssetCriticality,
|
||||
deleteAssetCriticality,
|
||||
fetchAssetCriticality,
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { AddDataSourcePanel } from './add_data_source';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
const mockUseNavigation = jest.fn().mockReturnValue({
|
||||
navigateTo: jest.fn(),
|
||||
});
|
||||
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
const original = jest.requireActual('../../../common/lib/kibana');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useNavigation: () => mockUseNavigation(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../common/hooks/integrations/use_integration_link_state', () => ({
|
||||
useIntegrationLinkState: jest.fn(() => ({})),
|
||||
}));
|
||||
|
||||
jest.mock('./hooks/use_integrations', () => ({
|
||||
useEntityAnalyticsIntegrations: jest.fn(() => [
|
||||
{
|
||||
name: 'Okta',
|
||||
version: '1.0.0',
|
||||
title: 'Okta Integration',
|
||||
description: 'Okta integration description',
|
||||
icon: 'oktaIcon',
|
||||
},
|
||||
{
|
||||
name: 'Entra ID',
|
||||
version: '1.0.0',
|
||||
title: 'Entra ID Integration',
|
||||
description: 'Entra ID integration description',
|
||||
icon: 'entraIdIcon',
|
||||
},
|
||||
{
|
||||
name: 'Active Directory',
|
||||
version: '1.0.0',
|
||||
title: 'Active Directory Integration',
|
||||
description: 'Active Directory integration description',
|
||||
icon: 'adIcon',
|
||||
},
|
||||
]),
|
||||
}));
|
||||
|
||||
describe('AddDataSourcePanel', () => {
|
||||
it('renders the panel title and description', () => {
|
||||
render(<AddDataSourcePanel />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.getByText('Add data source of your privileged users')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'To get started, define your privileged users by adding an integration with your organization’s user identities, select an index with the relevant data, or import your list of privileged users from a CSV file.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders integration cards and handles navigation on click', async () => {
|
||||
const mockNavigateTo = jest.fn();
|
||||
mockUseNavigation.mockReturnValue({
|
||||
navigateTo: mockNavigateTo,
|
||||
});
|
||||
|
||||
render(<AddDataSourcePanel />, { wrapper: TestProviders });
|
||||
|
||||
const integrationCards = await screen.findAllByTestId('entity_analytics-integration-card');
|
||||
expect(integrationCards.length).toBe(3);
|
||||
|
||||
const firstButton = integrationCards[0]?.querySelector('button')!;
|
||||
fireEvent.click(firstButton);
|
||||
expect(mockNavigateTo).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders the file import card', () => {
|
||||
render(<AddDataSourcePanel />, { wrapper: TestProviders });
|
||||
|
||||
const fileCard = screen.getByRole('button', { name: /file/i });
|
||||
expect(fileCard).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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, { Suspense, useState } from 'react';
|
||||
import {
|
||||
EuiCard,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
EuiSkeletonRectangle,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { IndexSelectorModal } from './select_index_modal';
|
||||
import { IntegrationCards } from './integrations_cards';
|
||||
|
||||
export const AddDataSourcePanel = () => {
|
||||
const [isIndexModalOpen, setIsIndexModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="xl" hasShadow={false} hasBorder={false}>
|
||||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.addDataSource.title"
|
||||
defaultMessage="Add data source of your privileged users"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="m">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.addDataSource.description"
|
||||
defaultMessage="To get started, define your privileged users by adding an integration with your organization’s user identities, select an index with the relevant data, or import your list of privileged users from a CSV file."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
<Suspense
|
||||
fallback={
|
||||
<EuiFlexGrid gutterSize="l" columns={3}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<EuiFlexItem grow={1} key={index}>
|
||||
<EuiSkeletonRectangle height="127px" width="100%" />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGrid>
|
||||
}
|
||||
>
|
||||
<IntegrationCards />
|
||||
</Suspense>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceAround" responsive={false}>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiHorizontalRule size="full" margin="none" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.addDataSource.or"
|
||||
defaultMessage="OR"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiHorizontalRule size="full" margin="none" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup direction="row" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiCard
|
||||
hasBorder
|
||||
layout="horizontal"
|
||||
icon={<EuiIcon size="l" type="indexOpen" />}
|
||||
titleSize="xs"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.addDataSource.index.title"
|
||||
defaultMessage="Index"
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.addDataSource.index.description"
|
||||
defaultMessage="Select an index that contains relevant user activity data"
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
setIsIndexModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
<IndexSelectorModal
|
||||
isOpen={isIndexModalOpen}
|
||||
onClose={() => {
|
||||
setIsIndexModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiCard
|
||||
hasBorder
|
||||
layout="horizontal"
|
||||
icon={<EuiIcon size="l" type="importAction" />}
|
||||
titleSize="xs"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.addDataSource.file.title"
|
||||
defaultMessage="File"
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.addDataSource.file.description"
|
||||
defaultMessage="Import a list of privileged users from a CSV, TXT, or TSV file"
|
||||
/>
|
||||
}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { useEntityAnalyticsRoutes } from '../../../api/api';
|
||||
|
||||
export const useFetchPrivilegedUserIndices = (query: string | undefined) => {
|
||||
const { searchPrivMonIndices } = useEntityAnalyticsRoutes();
|
||||
return useQuery(
|
||||
['POST', 'SEARCH_PRIVILEGED_USER_MONITORING_INDICES', query],
|
||||
({ signal }) => searchPrivMonIndices({ signal, query }),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
cacheTime: 0, // Do not cache the data because it is used by an autocomplete query
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { useGetPackageInfoByKeyQuery } from '@kbn/fleet-plugin/public';
|
||||
import type { GetInfoResponse } from '@kbn/fleet-plugin/common';
|
||||
|
||||
export const useEntityAnalyticsIntegrations = () => {
|
||||
const { data: okta } = useGetPackageInfoByKeyQuery(
|
||||
'entityanalytics_okta',
|
||||
undefined, // When package version is undefined it gets the latest version
|
||||
undefined, // No options required
|
||||
{
|
||||
suspense: true, // Make query suspend, it needs tu be wrapped by <Suspense />
|
||||
}
|
||||
);
|
||||
const { data: entra } = useGetPackageInfoByKeyQuery(
|
||||
'entityanalytics_entra_id',
|
||||
undefined, // When package version is undefined it gets the latest version
|
||||
undefined, // No options required
|
||||
{
|
||||
suspense: true,
|
||||
}
|
||||
);
|
||||
const { data: ad } = useGetPackageInfoByKeyQuery(
|
||||
'entityanalytics_ad',
|
||||
undefined, // When package version is undefined it gets the latest version
|
||||
{
|
||||
prerelease: true, // This is a technical preview package, delete this line when it is GA
|
||||
},
|
||||
{
|
||||
suspense: true,
|
||||
}
|
||||
);
|
||||
|
||||
const integrations = [okta, entra, ad]
|
||||
.filter<GetInfoResponse>(
|
||||
(integration): integration is GetInfoResponse => integration !== undefined
|
||||
)
|
||||
.map(({ item }) => item);
|
||||
|
||||
return integrations;
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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, { useCallback } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { LazyPackageCard } from '@kbn/fleet-plugin/public';
|
||||
import { useIntegrationLinkState } from '../../../common/hooks/integrations/use_integration_link_state';
|
||||
import { addPathParamToUrl } from '../../../common/utils/integrations';
|
||||
import { ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING_PATH } from '../../../../common/constants';
|
||||
import { useNavigation } from '../../../common/lib/kibana';
|
||||
import { INTEGRATION_APP_ID } from '../../../onboarding/components/onboarding_body/cards/integrations/constants';
|
||||
import { useEntityAnalyticsIntegrations } from './hooks/use_integrations';
|
||||
|
||||
/**
|
||||
* This component has to be wrapped by react Suspense.
|
||||
* It suspends while loading the lazy package card and while fetching the integrations.
|
||||
*/
|
||||
export const IntegrationCards = () => {
|
||||
const state = useIntegrationLinkState(ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING_PATH);
|
||||
const { navigateTo } = useNavigation();
|
||||
const integrations = useEntityAnalyticsIntegrations();
|
||||
const navigateToIntegration = useCallback(
|
||||
(id: string, version: string) => {
|
||||
navigateTo({
|
||||
appId: INTEGRATION_APP_ID,
|
||||
path: addPathParamToUrl(
|
||||
`/detail/${id}-${version}/overview`,
|
||||
ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING_PATH
|
||||
),
|
||||
state,
|
||||
});
|
||||
},
|
||||
[navigateTo, state]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="row" justifyContent="spaceBetween">
|
||||
{integrations.map(({ name, title, icons, description, version }) => (
|
||||
<EuiFlexItem grow={1} key={name} data-test-subj="entity_analytics-integration-card">
|
||||
<LazyPackageCard
|
||||
description={description ?? ''}
|
||||
icons={icons ?? []}
|
||||
id={name}
|
||||
name={name}
|
||||
title={title}
|
||||
version={version}
|
||||
onCardClick={() => {
|
||||
navigateToIntegration(name, version);
|
||||
}}
|
||||
// Required values that don't make sense for this scenario
|
||||
categories={[]}
|
||||
integration={''}
|
||||
url={''}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { ONBOARDING_VIDEO_SOURCE } from '../../../common/constants';
|
||||
import { AddDataSourcePanel } from './add_data_source';
|
||||
|
||||
const VIDEO_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.videoTitle',
|
||||
{
|
||||
defaultMessage: 'Onboarding Video',
|
||||
}
|
||||
);
|
||||
|
||||
export const PrivilegedUserMonitoringOnboardingPanel = () => {
|
||||
return (
|
||||
<EuiPanel paddingSize="none">
|
||||
<EuiPanel paddingSize="xl" color="subdued" hasShadow={false} hasBorder={false}>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
justifyContent="spaceBetween"
|
||||
gutterSize="xl"
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem grow={1} paddingSize="xl">
|
||||
<EuiPanel paddingSize="s" hasShadow={false} hasBorder={false} color="subdued">
|
||||
<EuiFlexGroup justifyContent="spaceBetween" direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.pageTitle"
|
||||
defaultMessage="Privileged user monitoring"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="m">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.pageDescription"
|
||||
defaultMessage="The Privileged user monitoring provides visibility into privileged user
|
||||
activity, helping security teams analyze account usage, track access events, and
|
||||
spot potential risks. By continuously monitoring high-risk accounts, the
|
||||
dashboard enables early detection of potential threats, such as unauthorized
|
||||
access or lateral movement, before they escalate."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" size="s">
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiIcon type="documentation" size="m" />
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.learnMore.label"
|
||||
defaultMessage="Want to learn more?"
|
||||
/>
|
||||
<EuiLink
|
||||
css={{ color: '#FF007F' }} // TODO DELETE THIS WHEN THE HREF LINK IS READY
|
||||
external={true}
|
||||
data-test-subj="learnMoreLink"
|
||||
href="??????" // TODO Add Link to docs
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.docLink.description"
|
||||
defaultMessage="Check our documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>
|
||||
<iframe
|
||||
css={css`
|
||||
height: auto;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
max-width: 480px;
|
||||
`}
|
||||
className="eui-alignMiddle"
|
||||
style={{ border: '8px solid #FF007F' }} // TODO DELETE THIS LINE WHEN THE VIDEO IS READY
|
||||
referrerPolicy="no-referrer"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
src={ONBOARDING_VIDEO_SOURCE}
|
||||
title={VIDEO_TITLE}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<AddDataSourcePanel />
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { EuiPanel, EuiSpacer, EuiTextColor, EuiTitle, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
export const PrivilegedUserMonitoringSampleDashboardsPanel = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<EuiPanel
|
||||
color="warning"
|
||||
hasBorder={false}
|
||||
paddingSize="none"
|
||||
className="test"
|
||||
css={css`
|
||||
border: ${euiTheme.border.width.thick} solid ${euiTheme.colors.warning};
|
||||
`}
|
||||
>
|
||||
<DashboardsSectionHeader />
|
||||
<EuiPanel hasShadow={false} hasBorder={false}>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="xxl" />
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
const DashboardsSectionHeader = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<EuiPanel
|
||||
color="warning"
|
||||
hasBorder={false}
|
||||
hasShadow={false}
|
||||
paddingSize="s"
|
||||
css={css`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: ${euiTheme.levels.header};
|
||||
top: var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0));
|
||||
`}
|
||||
>
|
||||
<EuiTitle size="xxs" className="eui-textCenter" textTransform="uppercase">
|
||||
<h3>
|
||||
<EuiTextColor color="warning">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.sampleDashboard.title"
|
||||
defaultMessage="Sample Dashboard"
|
||||
/>
|
||||
</EuiTextColor>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { IndexSelectorModal } from './select_index_modal';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
jest.mock('../../../common/hooks/use_app_toasts', () => ({
|
||||
useAppToasts: () => ({
|
||||
addError: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockUseFetchPrivilegedUserIndices = jest.fn().mockReturnValue({
|
||||
data: ['index1', 'index2'],
|
||||
isFetching: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
jest.mock('./hooks/use_fetch_privileged_user_indices', () => ({
|
||||
useFetchPrivilegedUserIndices: () => mockUseFetchPrivilegedUserIndices(),
|
||||
}));
|
||||
|
||||
describe('IndexSelectorModal', () => {
|
||||
const onCloseMock = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the modal when isOpen is true', () => {
|
||||
render(<IndexSelectorModal isOpen={true} onClose={onCloseMock} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(screen.getByText('Select index')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the modal when isOpen is false', () => {
|
||||
render(<IndexSelectorModal isOpen={false} onClose={onCloseMock} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Select index')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when the cancel button is clicked', () => {
|
||||
render(<IndexSelectorModal isOpen={true} onClose={onCloseMock} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Cancel'));
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('displays the indices in the combo box', () => {
|
||||
render(<IndexSelectorModal isOpen={true} onClose={onCloseMock} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByPlaceholderText('Select index'));
|
||||
|
||||
expect(screen.getByText('index1')).toBeInTheDocument();
|
||||
expect(screen.getByText('index2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows an error callout when there is an error', () => {
|
||||
mockUseFetchPrivilegedUserIndices.mockReturnValue({
|
||||
data: undefined,
|
||||
isFetching: false,
|
||||
error: new Error('Test error'),
|
||||
});
|
||||
|
||||
render(<IndexSelectorModal isOpen={true} onClose={onCloseMock} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(screen.getByText('Error loading indices. Please try again later.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays a loading state when fetching indices', () => {
|
||||
mockUseFetchPrivilegedUserIndices.mockReturnValue({
|
||||
data: null,
|
||||
isFetching: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(<IndexSelectorModal isOpen={true} onClose={onCloseMock} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(screen.queryByLabelText('Select index')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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, { useEffect, useMemo, useState } from 'react';
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiButton,
|
||||
EuiModalFooter,
|
||||
EuiSpacer,
|
||||
EuiComboBox,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalHeader,
|
||||
EuiModalBody,
|
||||
EuiModal,
|
||||
EuiFormRow,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDebounceFn } from '@kbn/react-hooks';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
import { useFetchPrivilegedUserIndices } from './hooks/use_fetch_privileged_user_indices';
|
||||
|
||||
const SELECT_INDEX_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.selectIndex.comboboxPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select index',
|
||||
}
|
||||
);
|
||||
|
||||
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 IndexSelectorModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { addError } = useAppToasts();
|
||||
const [searchQuery, setSearchQuery] = useState<string | undefined>(undefined);
|
||||
const { data: indices, isFetching, error } = useFetchPrivilegedUserIndices(searchQuery);
|
||||
const [selectedOptions, setSelected] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
|
||||
const debouncedSetSearchQuery = useDebounceFn(setSearchQuery, DEBOUNCE_OPTIONS);
|
||||
const options = useMemo(
|
||||
() =>
|
||||
indices?.map((index) => ({
|
||||
label: index,
|
||||
})) ?? [],
|
||||
[indices]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (error != null) {
|
||||
addError(error, { title: LOADING_ERROR_MESSAGE });
|
||||
}
|
||||
}, [addError, error]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiModal onClose={onClose} maxWidth="624px">
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.selectIndex.title"
|
||||
defaultMessage="Select index"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.selectIndex.description"
|
||||
defaultMessage="Add your privileged users by selecting one or more indices as data source. All user names in the indices, specified in user.name field, will be defined as privileged users."
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
{error ? (
|
||||
<>
|
||||
<EuiCallOut color="danger">{LOADING_ERROR_MESSAGE}</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null}
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.selectIndex.comboboxLabel"
|
||||
defaultMessage="Index"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<EuiComboBox
|
||||
isLoading={isFetching && !error}
|
||||
fullWidth
|
||||
aria-label={SELECT_INDEX_LABEL}
|
||||
placeholder={SELECT_INDEX_LABEL}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={setSelected}
|
||||
isClearable={true}
|
||||
onSearchChange={(query) => {
|
||||
debouncedSetSearchQuery.run(query);
|
||||
}}
|
||||
async
|
||||
optionMatcher={(_) => false} // prevent the combo box from searching on the client side
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="plusInCircle" onClick={() => {}}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.selectIndex.createIndexButtonLabel"
|
||||
defaultMessage="Create index"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiButtonEmpty onClick={onClose}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.selectIndex.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton onClick={() => {}} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.selectIndex.addUserButtonLabel"
|
||||
defaultMessage="Add privileged users"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -17,19 +17,13 @@ import { ENTITY_ANALYTICS, ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING } from '.
|
|||
import privilegedUserMonitoringPageImg from '../common/images/privileged_user_monitoring_page.png';
|
||||
|
||||
const privMonLinks: LinkItem = {
|
||||
id: SecurityPageName.privilegedUserMonitoring,
|
||||
id: SecurityPageName.entityAnalyticsPrivilegedUserMonitoring,
|
||||
title: ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING,
|
||||
landingImage: privilegedUserMonitoringPageImg,
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.appLinks.privilegedUserMonitoring.Description',
|
||||
{
|
||||
defaultMessage: '???????????????????', // TODO
|
||||
}
|
||||
),
|
||||
path: ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING_PATH,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.privilegedUserMonitoring', {
|
||||
defaultMessage: 'Privileged User Monitoring',
|
||||
defaultMessage: 'Privileged user monitoring',
|
||||
}),
|
||||
],
|
||||
experimentalKey: 'privilegeMonitoringEnabled',
|
||||
|
@ -46,7 +40,7 @@ export const entityAnalyticsLinks: LinkItem = {
|
|||
globalNavPosition: 10,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.entityAnalytics.landing', {
|
||||
defaultMessage: 'Entity Analytics',
|
||||
defaultMessage: 'Entity analytics',
|
||||
}),
|
||||
],
|
||||
links: [privMonLinks],
|
||||
|
|
|
@ -16,7 +16,7 @@ import { trackLandingLinkClick } from '../../common/lib/telemetry/trackers';
|
|||
import { useGlobalQueryString } from '../../common/utils/global_query_string';
|
||||
|
||||
const PAGE_TITLE = i18n.translate('xpack.securitySolution.entityAnalytics.landing.pageTitle', {
|
||||
defaultMessage: 'Entity Analytics',
|
||||
defaultMessage: 'Entity analytics',
|
||||
});
|
||||
|
||||
export const EntityAnalyticsLandingPage = () => {
|
||||
|
|
|
@ -5,24 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { SecurityPageName } from '../../app/types';
|
||||
import { HeaderPage } from '../../common/components/header_page';
|
||||
import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
|
||||
import { SpyRoute } from '../../common/utils/route/spy_routes';
|
||||
|
||||
const PAGE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.pageTitle',
|
||||
{
|
||||
defaultMessage: 'Privileged User Monitoring',
|
||||
}
|
||||
);
|
||||
import { PrivilegedUserMonitoringSampleDashboardsPanel } from '../components/privileged_user_monitoring_onboarding/sample_dashboards_panel';
|
||||
import { PrivilegedUserMonitoringOnboardingPanel } from '../components/privileged_user_monitoring_onboarding/onboarding_panel';
|
||||
|
||||
export const EntityAnalyticsPrivilegedUserMonitoringPage = () => {
|
||||
return (
|
||||
<SecuritySolutionPageWrapper>
|
||||
<HeaderPage title={PAGE_TITLE} />
|
||||
<SpyRoute pageName={SecurityPageName.privilegedUserMonitoring} />
|
||||
<PrivilegedUserMonitoringOnboardingPanel />
|
||||
<EuiSpacer size="l" />
|
||||
<PrivilegedUserMonitoringSampleDashboardsPanel />
|
||||
<SpyRoute pageName={SecurityPageName.entityAnalyticsPrivilegedUserMonitoring} />
|
||||
</SecuritySolutionPageWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -82,7 +82,7 @@ describe('EntityStoreManagementPage', () => {
|
|||
render(<EntityStoreManagementPage />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.getByTestId('entityStoreManagementPage')).toBeInTheDocument();
|
||||
expect(screen.getByText('Entity Store')).toBeInTheDocument();
|
||||
expect(screen.getByText('Entity store')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables the switch when status is installing', () => {
|
||||
|
|
|
@ -67,7 +67,7 @@ const isEntityStoreInstalled = (status?: StoreStatus) => status && status !== 'n
|
|||
const entityStoreLabel = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.entityStoreManagementPage.title',
|
||||
{
|
||||
defaultMessage: 'Entity Store',
|
||||
defaultMessage: 'Entity store',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -167,7 +167,7 @@ export const routes = [
|
|||
path: ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING_PATH,
|
||||
component: withSecurityRoutePageWrapper(
|
||||
EntityAnalyticsPrivilegedUserMonitoringContainer,
|
||||
SecurityPageName.privilegedUserMonitoring,
|
||||
SecurityPageName.entityAnalyticsPrivilegedUserMonitoring,
|
||||
{
|
||||
redirectOnMissing: true,
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ import { IconAssetCriticality } from '../common/icons/asset_criticality';
|
|||
const categories = [
|
||||
{
|
||||
label: i18n.translate('xpack.securitySolution.appLinks.category.entityAnalytics', {
|
||||
defaultMessage: 'Entity Analytics',
|
||||
defaultMessage: 'Entity analytics',
|
||||
}),
|
||||
linkIds: [
|
||||
SecurityPageName.entityAnalyticsManagement,
|
||||
|
|
|
@ -18,8 +18,6 @@ export const FLEET_APP_ID = `fleet`;
|
|||
export const INTEGRATION_APP_ID = `integrations`;
|
||||
export const LOADING_SKELETON_TEXT_LINES = 10; // 10 lines of text
|
||||
export const MAX_CARD_HEIGHT_IN_PX = 127; // px
|
||||
export const RETURN_APP_ID = 'returnAppId';
|
||||
export const RETURN_PATH = 'returnPath';
|
||||
export const SCROLL_ELEMENT_ID = 'integrations-scroll-container';
|
||||
export const SEARCH_FILTER_CATEGORIES: CategoryFacet[] = [];
|
||||
export const WITH_SEARCH_BOX_HEIGHT = '568px';
|
||||
|
|
|
@ -7,34 +7,20 @@
|
|||
import { useMemo } from 'react';
|
||||
import type { IntegrationCardItem } from '@kbn/fleet-plugin/public';
|
||||
import { SECURITY_UI_APP_ID } from '@kbn/security-solution-navigation';
|
||||
import { getIntegrationLinkState } from '../../../../../common/hooks/integrations/use_integration_link_state';
|
||||
import { addPathParamToUrl } from '../../../../../common/utils/integrations';
|
||||
import { useNavigation } from '../../../../../common/lib/kibana';
|
||||
import {
|
||||
APP_INTEGRATIONS_PATH,
|
||||
APP_UI_ID,
|
||||
ONBOARDING_PATH,
|
||||
} from '../../../../../../common/constants';
|
||||
import { APP_INTEGRATIONS_PATH, ONBOARDING_PATH } from '../../../../../../common/constants';
|
||||
import {
|
||||
CARD_DESCRIPTION_LINE_CLAMP,
|
||||
CARD_TITLE_LINE_CLAMP,
|
||||
INTEGRATION_APP_ID,
|
||||
MAX_CARD_HEIGHT_IN_PX,
|
||||
RETURN_APP_ID,
|
||||
RETURN_PATH,
|
||||
TELEMETRY_INTEGRATION_CARD,
|
||||
} from './constants';
|
||||
import type { GetAppUrl, NavigateTo } from '../../../../../common/lib/kibana';
|
||||
import { trackOnboardingLinkClick } from '../../../lib/telemetry';
|
||||
|
||||
const addPathParamToUrl = (url: string, onboardingLink: string) => {
|
||||
const encoded = encodeURIComponent(onboardingLink);
|
||||
const paramsString = `${RETURN_PATH}=${encoded}&${RETURN_APP_ID}=${APP_UI_ID}`;
|
||||
|
||||
if (url.indexOf('?') >= 0) {
|
||||
return `${url}&${paramsString}`;
|
||||
}
|
||||
return `${url}?${paramsString}`;
|
||||
};
|
||||
|
||||
const extractFeaturedCards = (filteredCards: IntegrationCardItem[], featuredCardIds: string[]) => {
|
||||
return filteredCards.reduce<IntegrationCardItem[]>((acc, card) => {
|
||||
if (featuredCardIds.includes(card.id)) {
|
||||
|
@ -70,7 +56,7 @@ const getFilteredCards = ({
|
|||
};
|
||||
};
|
||||
|
||||
const addSecuritySpecificProps = ({
|
||||
export const addSecuritySpecificProps = ({
|
||||
navigateTo,
|
||||
getAppUrl,
|
||||
card,
|
||||
|
@ -82,11 +68,7 @@ const addSecuritySpecificProps = ({
|
|||
}): IntegrationCardItem => {
|
||||
const onboardingLink = getAppUrl({ appId: SECURITY_UI_APP_ID, path: ONBOARDING_PATH });
|
||||
const integrationRootUrl = getAppUrl({ appId: INTEGRATION_APP_ID });
|
||||
const state = {
|
||||
onCancelNavigateTo: [APP_UI_ID, { path: ONBOARDING_PATH }],
|
||||
onCancelUrl: onboardingLink,
|
||||
onSaveNavigateTo: [APP_UI_ID, { path: ONBOARDING_PATH }],
|
||||
};
|
||||
const state = getIntegrationLinkState(ONBOARDING_PATH, getAppUrl);
|
||||
const url =
|
||||
card.url.indexOf(APP_INTEGRATIONS_PATH) >= 0 && onboardingLink
|
||||
? addPathParamToUrl(card.url, ONBOARDING_PATH)
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EXCLUDE_ELASTIC_CLOUD_INDICES, INCLUDE_INDEX_PATTERN } from '../../../../common/constants';
|
||||
export const SCOPE = ['securitySolution'];
|
||||
export const TYPE = 'entity_analytics:monitoring:privileges:engine';
|
||||
export const VERSION = '1.0.0';
|
||||
|
@ -21,3 +22,12 @@ export const PRIVILEGE_MONITORING_ENGINE_STATUS = {
|
|||
// Base constants
|
||||
export const PRIVMON_BASE_PREFIX = 'privmon' as const;
|
||||
export const PRIVILEGE_MONITORING_INTERNAL_INDICES_PATTERN = `.${PRIVMON_BASE_PREFIX}*` as const;
|
||||
|
||||
// Indices that are exclude from the search
|
||||
export const PRE_EXCLUDE_INDICES: string[] = [
|
||||
...INCLUDE_INDEX_PATTERN.map((index) => `-${index}`),
|
||||
...EXCLUDE_ELASTIC_CLOUD_INDICES,
|
||||
];
|
||||
|
||||
// Indices that are excludes from the search result (This patterns can't be excluded from the search)
|
||||
export const POST_EXCLUDE_INDICES = ['.']; // internal indices
|
||||
|
|
|
@ -27,7 +27,11 @@ import { startPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task'
|
|||
import { createOrUpdateIndex } from '../utils/create_or_update_index';
|
||||
import { generateUserIndexMappings, getPrivilegedMonitorUsersIndex } from './indices';
|
||||
import { PrivilegeMonitoringEngineDescriptorClient } from './saved_object/privilege_monitoring';
|
||||
import { PRIVILEGE_MONITORING_ENGINE_STATUS } from './constants';
|
||||
import {
|
||||
POST_EXCLUDE_INDICES,
|
||||
PRE_EXCLUDE_INDICES,
|
||||
PRIVILEGE_MONITORING_ENGINE_STATUS,
|
||||
} from './constants';
|
||||
import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../audit';
|
||||
import { PrivilegeMonitoringEngineActions } from './auditing/actions';
|
||||
import {
|
||||
|
@ -135,6 +139,28 @@ export class PrivilegeMonitoringDataClient {
|
|||
});
|
||||
}
|
||||
|
||||
public async searchPrivilegesIndices(query: string | undefined) {
|
||||
const { indices } = await this.esClient.fieldCaps({
|
||||
index: [query ? `*${query}*` : '*', ...PRE_EXCLUDE_INDICES],
|
||||
types: ['keyword'],
|
||||
fields: ['user.name'], // search for indices with field 'user.name' of type 'keyword'
|
||||
include_unmapped: false,
|
||||
ignore_unavailable: true,
|
||||
allow_no_indices: true,
|
||||
expand_wildcards: 'open',
|
||||
include_empty_fields: false,
|
||||
filters: '-parent',
|
||||
});
|
||||
|
||||
if (!Array.isArray(indices) || indices.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return indices.filter(
|
||||
(name) => !POST_EXCLUDE_INDICES.some((pattern) => name.startsWith(pattern))
|
||||
);
|
||||
}
|
||||
|
||||
public getIndex() {
|
||||
return getPrivilegedMonitorUsersIndex(this.opts.namespace);
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import type { EntityAnalyticsRoutesDeps } from '../../types';
|
||||
import { healthCheckPrivilegeMonitoringRoute } from './health';
|
||||
|
||||
import { initPrivilegeMonitoringEngineRoute } from './init';
|
||||
import { searchPrivilegeMonitoringIndicesRoute } from './search_indices';
|
||||
|
||||
export const registerPrivilegeMonitoringRoutes = ({
|
||||
router,
|
||||
|
@ -18,4 +18,5 @@ export const registerPrivilegeMonitoringRoutes = ({
|
|||
}: EntityAnalyticsRoutesDeps) => {
|
||||
initPrivilegeMonitoringEngineRoute(router, logger, config);
|
||||
healthCheckPrivilegeMonitoringRoute(router, logger, config);
|
||||
searchPrivilegeMonitoringIndicesRoute(router, logger, config);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { IKibanaResponse, Logger } from '@kbn/core/server';
|
||||
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { take } from 'lodash/fp';
|
||||
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
||||
import { API_VERSIONS, APP_ID } from '../../../../../common/constants';
|
||||
import type { EntityAnalyticsRoutesDeps } from '../../types';
|
||||
import { SearchPrivilegesIndicesRequestQuery } from '../../../../../common/api/entity_analytics/monitoring';
|
||||
|
||||
// Return a subset of all indices that contain the user.name field
|
||||
const LIMIT = 20;
|
||||
|
||||
export const searchPrivilegeMonitoringIndicesRoute = (
|
||||
router: EntityAnalyticsRoutesDeps['router'],
|
||||
logger: Logger,
|
||||
config: EntityAnalyticsRoutesDeps['config']
|
||||
) => {
|
||||
router.versioned
|
||||
.get({
|
||||
access: 'public',
|
||||
path: '/api/entity_analytics/monitoring/privileges/indices',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {
|
||||
request: {
|
||||
query: buildRouteValidationWithZod(SearchPrivilegesIndicesRequestQuery),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
async (context, request, response): Promise<IKibanaResponse<{}>> => {
|
||||
const secSol = await context.securitySolution;
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
const query = request.query.searchQuery;
|
||||
|
||||
try {
|
||||
const indices = await secSol
|
||||
.getPrivilegeMonitoringDataClient()
|
||||
.searchPrivilegesIndices(query);
|
||||
|
||||
return response.ok({
|
||||
body: take(LIMIT, indices),
|
||||
});
|
||||
} catch (e) {
|
||||
const error = transformError(e);
|
||||
logger.error(`Error searching privilege monitoring indices: ${error.message}`);
|
||||
return siemResponse.error({
|
||||
statusCode: error.statusCode,
|
||||
body: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -11,6 +11,7 @@ import { registerRiskEngineRoutes } from './risk_engine/routes';
|
|||
import type { EntityAnalyticsRoutesDeps } from './types';
|
||||
import { registerEntityStoreRoutes } from './entity_store/routes';
|
||||
import { registerPrivilegeMonitoringRoutes } from './privilege_monitoring/routes/register_privilege_monitoring_routes';
|
||||
|
||||
export const registerEntityAnalyticsRoutes = (routeDeps: EntityAnalyticsRoutesDeps) => {
|
||||
registerAssetCriticalityRoutes(routeDeps);
|
||||
registerRiskScoreRoutes(routeDeps);
|
||||
|
@ -18,6 +19,7 @@ export const registerEntityAnalyticsRoutes = (routeDeps: EntityAnalyticsRoutesDe
|
|||
if (!routeDeps.config.experimentalFeatures.entityStoreDisabled) {
|
||||
registerEntityStoreRoutes(routeDeps);
|
||||
}
|
||||
|
||||
if (routeDeps.config.experimentalFeatures.privilegeMonitoringEnabled) {
|
||||
registerPrivilegeMonitoringRoutes(routeDeps);
|
||||
}
|
||||
|
|
|
@ -386,7 +386,7 @@ Object {
|
|||
"link": "securitySolutionUI:entity_analytics-landing",
|
||||
"renderAs": "panelOpener",
|
||||
"spaceBefore": null,
|
||||
"title": "Entity Analytics",
|
||||
"title": "Entity analytics",
|
||||
},
|
||||
Object {
|
||||
"breadcrumbStatus": "hidden",
|
||||
|
@ -479,13 +479,13 @@ Object {
|
|||
"id": "entity_analytics-management",
|
||||
"link": "securitySolutionUI:entity_analytics-management",
|
||||
"sideNavStatus": "hidden",
|
||||
"title": "Entity Risk Score",
|
||||
"title": "Entity risk score",
|
||||
},
|
||||
Object {
|
||||
"id": "entity_analytics-entity_store_management",
|
||||
"link": "securitySolutionUI:entity_analytics-entity_store_management",
|
||||
"sideNavStatus": "hidden",
|
||||
"title": "Entity Store",
|
||||
"title": "Entity store",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -446,7 +446,7 @@ const createNavigationTree$ = (services: Services): Rx.Observable<NavigationTree
|
|||
children: [
|
||||
{
|
||||
id: 'entity_analytics-privileged_user_monitoring',
|
||||
link: securityLink(SecurityPageName.privilegedUserMonitoring),
|
||||
link: securityLink(SecurityPageName.entityAnalyticsPrivilegedUserMonitoring),
|
||||
renderAs: 'item',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -372,7 +372,7 @@ Object {
|
|||
"link": "securitySolutionUI:entity_analytics-landing",
|
||||
"renderAs": "panelOpener",
|
||||
"spaceBefore": null,
|
||||
"title": "Entity Analytics",
|
||||
"title": "Entity analytics",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
|
@ -458,13 +458,13 @@ Object {
|
|||
"id": "entity_analytics-management",
|
||||
"link": "securitySolutionUI:entity_analytics-management",
|
||||
"sideNavStatus": "hidden",
|
||||
"title": "Entity Risk Score",
|
||||
"title": "Entity risk score",
|
||||
},
|
||||
Object {
|
||||
"id": "entity_analytics-entity_store_management",
|
||||
"link": "securitySolutionUI:entity_analytics-entity_store_management",
|
||||
"sideNavStatus": "hidden",
|
||||
"title": "Entity Store",
|
||||
"title": "Entity store",
|
||||
},
|
||||
],
|
||||
"defaultIsCollapsed": false,
|
||||
|
|
|
@ -424,7 +424,7 @@ export const createSecurityNavigationTree$ = (
|
|||
children: [
|
||||
{
|
||||
id: 'entity_analytics-privileged_user_monitoring',
|
||||
link: securityLink(SecurityPageName.privilegedUserMonitoring),
|
||||
link: securityLink(SecurityPageName.entityAnalyticsPrivilegedUserMonitoring),
|
||||
renderAs: 'item',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -137,6 +137,7 @@ import {
|
|||
} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_preview/rule_preview.gen';
|
||||
import { RunScriptActionRequestBodyInput } from '@kbn/security-solution-plugin/common/api/endpoint/actions/response_actions/run_script/run_script.gen';
|
||||
import { SearchAlertsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/query_signals/query_signals_route.gen';
|
||||
import { SearchPrivilegesIndicesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/monitoring/search_indices.gen';
|
||||
import { SetAlertAssigneesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen';
|
||||
import { SetAlertsStatusRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/set_signal_status/set_signals_status_route.gen';
|
||||
import { SetAlertTagsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_tags/set_alert_tags/set_alert_tags.gen';
|
||||
|
@ -1517,6 +1518,14 @@ The difference between the `id` and `rule_id` is that the `id` is a unique rule
|
|||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.send(props.body as object);
|
||||
},
|
||||
searchPrivilegesIndices(props: SearchPrivilegesIndicesProps, kibanaSpace: string = 'default') {
|
||||
return supertest
|
||||
.get(routeWithNamespace('/api/entity_analytics/monitoring/privileges/indices', kibanaSpace))
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.query(props.query);
|
||||
},
|
||||
/**
|
||||
* Assign users to detection alerts, and unassign them from alerts.
|
||||
> info
|
||||
|
@ -1951,6 +1960,9 @@ export interface RunScriptActionProps {
|
|||
export interface SearchAlertsProps {
|
||||
body: SearchAlertsRequestBodyInput;
|
||||
}
|
||||
export interface SearchPrivilegesIndicesProps {
|
||||
query: SearchPrivilegesIndicesRequestQueryInput;
|
||||
}
|
||||
export interface SetAlertAssigneesProps {
|
||||
body: SetAlertAssigneesRequestBodyInput;
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../../ftr_provider_context';
|
|||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('Entity Analytics - Privilege Monitoring', function () {
|
||||
loadTestFile(require.resolve('./engine'));
|
||||
loadTestFile(require.resolve('./search_indices'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const api = getService('securitySolutionApi');
|
||||
const es = getService('es');
|
||||
const log = getService('log');
|
||||
|
||||
const logWhenNot200 = (res: any) => {
|
||||
if (res.status !== 200) {
|
||||
log.error(`Search index failed`);
|
||||
log.error(JSON.stringify(res.body));
|
||||
}
|
||||
};
|
||||
|
||||
const indexName = 'user-index-test';
|
||||
|
||||
describe('@ess @serverless @skipInServerlessMKI EntityAnalytics Monitoring SearchIndices', () => {
|
||||
before(async () => {
|
||||
await es.indices.create({ index: indexName });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await es.indices.delete({ index: indexName, ignore_unavailable: true });
|
||||
});
|
||||
|
||||
describe('search_indices API', () => {
|
||||
it('should return an empty array if no indices match the search query', async () => {
|
||||
const res = await api.searchPrivilegesIndices({ query: { searchQuery: 'test_1235678' } });
|
||||
|
||||
logWhenNot200(res);
|
||||
|
||||
expect(res.status).eql(200);
|
||||
expect(res.body).eql([]);
|
||||
});
|
||||
|
||||
it('should return all indices when no searchQuery is given', async () => {
|
||||
const res = await api.searchPrivilegesIndices({ query: { searchQuery: undefined } });
|
||||
|
||||
logWhenNot200(res);
|
||||
|
||||
expect(res.status).eql(200);
|
||||
expect(res.body).to.contain(indexName);
|
||||
});
|
||||
|
||||
it('should return index when searchQuery matches', async () => {
|
||||
const res = await api.searchPrivilegesIndices({ query: { searchQuery: indexName } });
|
||||
|
||||
logWhenNot200(res);
|
||||
|
||||
expect(res.status).eql(200);
|
||||
expect(res.body.length).eql(1);
|
||||
expect(res.body).to.contain(indexName);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -29,7 +29,7 @@ describe(
|
|||
});
|
||||
|
||||
it('renders page as expected', () => {
|
||||
cy.get(PAGE_TITLE).should('include.text', 'Entity Store');
|
||||
cy.get(PAGE_TITLE).should('include.text', 'Entity store');
|
||||
});
|
||||
|
||||
it('uploads a file', () => {
|
||||
|
|
|
@ -46,7 +46,7 @@ describe(
|
|||
});
|
||||
|
||||
it('renders page as expected', () => {
|
||||
cy.get(PAGE_TITLE).should('have.text', 'Entity Risk Score');
|
||||
cy.get(PAGE_TITLE).should('have.text', 'Entity risk score');
|
||||
});
|
||||
|
||||
describe('Risk preview', () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue