diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index d5bde93f46c9..ca356d87411d 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -11172,8 +11172,21 @@ paths: schema: type: object properties: - ok: - type: boolean + error: + type: object + properties: + message: + type: string + required: + - status + status: + oneOf: + - $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineStatus' + - enum: + - not_found + type: string + required: + - status description: Successful response summary: Health check on Privilege Monitoring tags: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index f1809a523b66..0a330c77dadb 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -13331,8 +13331,21 @@ paths: schema: type: object properties: - ok: - type: boolean + error: + type: object + properties: + message: + type: string + required: + - status + status: + oneOf: + - $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineStatus' + - enum: + - not_found + type: string + required: + - status description: Successful response summary: Health check on Privilege Monitoring tags: diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/monitoring/create_index.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/monitoring/create_index.gen.ts new file mode 100644 index 000000000000..820285f8cfde --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/monitoring/create_index.gen.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Create an index for Privileges Monitoring import + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; + +export type CreatePrivilegesImportIndexRequestBody = z.infer< + typeof CreatePrivilegesImportIndexRequestBody +>; +export const CreatePrivilegesImportIndexRequestBody = z.object({ + /** + * The index name to create + */ + name: z.string(), + /** + * The mode of index creation, either 'standard' or 'lookup' + */ + mode: z.enum(['standard', 'lookup']), +}); +export type CreatePrivilegesImportIndexRequestBodyInput = z.input< + typeof CreatePrivilegesImportIndexRequestBody +>; + +export type CreatePrivilegesImportIndexResponse = z.infer< + typeof CreatePrivilegesImportIndexResponse +>; +export const CreatePrivilegesImportIndexResponse = z.object({ + success: z.boolean().optional(), +}); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/monitoring/create_index.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/monitoring/create_index.schema.yaml new file mode 100644 index 000000000000..d2ab049dbca8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/monitoring/create_index.schema.yaml @@ -0,0 +1,42 @@ +openapi: 3.0.0 + +info: + title: Create an index for Privileges Monitoring import + version: '2023-10-31' +paths: + /api/entity_analytics/monitoring/privileges/indices: + put: + x-labels: [ess, serverless] + x-internal: true + x-codegen-enabled: true + operationId: CreatePrivilegesImportIndex + summary: Create an index for Privileges Monitoring import + + requestBody: + description: Schema for the entity store initialization + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: The index name to create + mode: + type: string + enum: [standard, lookup] + description: The mode of index creation, either 'standard' or 'lookup' + required: + - name + - mode + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + success: + type: boolean \ No newline at end of file diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/health.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/health.gen.ts index 675094123c1c..e775d4e0c054 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/health.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/health.gen.ts @@ -16,7 +16,14 @@ import { z } from '@kbn/zod'; +import { EngineStatus } from './common.gen'; + export type PrivMonHealthResponse = z.infer; export const PrivMonHealthResponse = z.object({ - ok: z.boolean().optional(), + status: z.union([EngineStatus, z.literal('not_found')]), + error: z + .object({ + message: z.string().optional(), + }) + .optional(), }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/health.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/health.schema.yaml index 2b712852f64f..8c0cebff8252 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/health.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/health.schema.yaml @@ -19,5 +19,18 @@ paths: schema: type: object properties: - ok: - type: boolean + status: + oneOf: + - $ref: './common.schema.yaml#/components/schemas/EngineStatus' + - type: string + enum: [not_found] + error: + type: object + required: + - status + properties: + message: + type: string + + required: + - status diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts index 4dfa65598166..d9bdb770867a 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -259,6 +259,10 @@ import type { GetEntityStoreStatusResponse, } from './entity_analytics/entity_store/status.gen'; import type { RunEntityAnalyticsMigrationsResponse } from './entity_analytics/migrations/run_migrations_route.gen'; +import type { + CreatePrivilegesImportIndexRequestBodyInput, + CreatePrivilegesImportIndexResponse, +} from './entity_analytics/monitoring/create_index.gen'; import type { SearchPrivilegesIndicesRequestQueryInput, SearchPrivilegesIndicesResponse, @@ -623,6 +627,19 @@ If a record already exists for the specified entity, that record is overwritten }) .catch(catchAxiosErrorFormatAndThrow); } + async createPrivilegesImportIndex(props: CreatePrivilegesImportIndexProps) { + this.log.info(`${new Date().toISOString()} Calling API CreatePrivilegesImportIndex`); + return this.kbnClient + .request({ + path: '/api/entity_analytics/monitoring/privileges/indices', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'PUT', + body: props.body, + }) + .catch(catchAxiosErrorFormatAndThrow); + } async createPrivMonUser(props: CreatePrivMonUserProps) { this.log.info(`${new Date().toISOString()} Calling API CreatePrivMonUser`); return this.kbnClient @@ -2615,6 +2632,9 @@ export interface CreateAlertsMigrationProps { export interface CreateAssetCriticalityRecordProps { body: CreateAssetCriticalityRecordRequestBodyInput; } +export interface CreatePrivilegesImportIndexProps { + body: CreatePrivilegesImportIndexRequestBodyInput; +} export interface CreatePrivMonUserProps { body: CreatePrivMonUserRequestBodyInput; } diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index a6636c78f307..c081177d2b54 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -329,8 +329,21 @@ paths: schema: type: object properties: - ok: - type: boolean + error: + type: object + properties: + message: + type: string + required: + - status + status: + oneOf: + - $ref: '#/components/schemas/EngineStatus' + - enum: + - not_found + type: string + required: + - status description: Successful response summary: Health check on Privilege Monitoring tags: diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index a041ea5fdc65..b598c94ca90c 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -329,8 +329,21 @@ paths: schema: type: object properties: - ok: - type: boolean + error: + type: object + properties: + message: + type: string + required: + - status + status: + oneOf: + - $ref: '#/components/schemas/EngineStatus' + - enum: + - not_found + type: string + required: + - status description: Successful response summary: Health check on Privilege Monitoring tags: diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts index 20e54e719e1c..04d25256ae97 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/api.ts @@ -6,6 +6,8 @@ */ import { useMemo } from 'react'; +import type { CreatePrivilegesImportIndexResponse } from '../../../common/api/entity_analytics/monitoring/create_index.gen'; +import type { PrivMonHealthResponse } from '../../../common/api/entity_analytics/privilege_monitoring/health.gen'; import type { InitMonitoringEngineResponse } from '../../../common/api/entity_analytics/privilege_monitoring/engine/init.gen'; import { PRIVMON_PUBLIC_INIT, @@ -209,6 +211,44 @@ export const useEntityAnalyticsRoutes = () => { } ); + /** + * Create an index for privilege monitoring import + */ + const createPrivMonImportIndex = async (params: { + name: string; + mode: 'standard' | 'lookup'; + signal?: AbortSignal; + }) => + http.fetch( + '/api/entity_analytics/monitoring/privileges/indices', + { + version: API_VERSIONS.public.v1, + method: 'PUT', + body: JSON.stringify({ + name: params.name, + mode: params.mode, + }), + signal: params.signal, + } + ); + /** + * Register a data source for privilege monitoring engine + */ + const registerPrivMonMonitoredIndices = async (indexPattern: string | undefined) => + http.fetch( + '/api/entity_analytics/monitoring/entity_source', + { + version: API_VERSIONS.public.v1, + method: 'POST', + + body: JSON.stringify({ + type: 'index', + name: 'User Monitored Indices', + indexPattern, + }), + } + ); + /** * Create asset criticality */ @@ -309,6 +349,12 @@ export const useEntityAnalyticsRoutes = () => { method: 'POST', }); + const fetchPrivilegeMonitoringEngineStatus = async (): Promise => + http.fetch('/api/entity_analytics/monitoring/privileges/health', { + version: API_VERSIONS.public.v1, + method: 'GET', + }); + /** * Fetches risk engine settings */ @@ -347,12 +393,15 @@ export const useEntityAnalyticsRoutes = () => { fetchAssetCriticalityPrivileges, fetchEntityStorePrivileges, searchPrivMonIndices, + createPrivMonImportIndex, createAssetCriticality, deleteAssetCriticality, fetchAssetCriticality, uploadAssetCriticalityFile, uploadPrivilegedUserMonitoringFile, initPrivilegedMonitoringEngine, + registerPrivMonMonitoredIndices, + fetchPrivilegeMonitoringEngineStatus, fetchRiskEngineSettings, calculateEntityRiskScore, cleanUpRiskEngine, diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/hooks/use_privileged_monitoring_engine_status.ts b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/hooks/use_privileged_monitoring_engine_status.ts new file mode 100644 index 000000000000..7cb3584155c5 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/api/hooks/use_privileged_monitoring_engine_status.ts @@ -0,0 +1,19 @@ +/* + * 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 type { SecurityAppError } from '@kbn/securitysolution-t-grid'; +import type { PrivMonHealthResponse } from '../../../../common/api/entity_analytics/privilege_monitoring/health.gen'; +import { useEntityAnalyticsRoutes } from '../api'; + +export const usePrivilegedMonitoringEngineStatus = () => { + const { fetchPrivilegeMonitoringEngineStatus } = useEntityAnalyticsRoutes(); + return useQuery({ + queryKey: ['GET', 'FETCH_PRIVILEGED_MONITORING_ENGINE_STATUS'], + queryFn: fetchPrivilegeMonitoringEngineStatus, + retry: 0, + }); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/index.tsx index 99a61c41177b..fc49dc9848e0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/index.tsx @@ -20,10 +20,12 @@ export interface OnboardingCallout { export const PrivilegedUserMonitoring = ({ callout, + error, onManageUserClicked, sourcererDataView, }: { callout?: OnboardingCallout; + error?: string; onManageUserClicked: () => void; sourcererDataView: DataViewSpec; }) => { @@ -36,6 +38,20 @@ export const PrivilegedUserMonitoring = ({ return ( + {error && ( + + } + color="danger" + iconType="cross" + > +

{error}

+
+ )} {callout && !dismissCallout && ( { } onClick={showIndexModal} /> - + {isIndexModalOpen && ( + + )}
({ + useEntityAnalyticsRoutes: () => ({ + createPrivMonImportIndex: mockCreatePrivMonImportIndex, + }), +})); + +const onCloseMock = jest.fn(); +const onCreateMock = jest.fn(); + +describe('CreateIndexModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders modal with form fields and buttons', () => { + render(, { + wrapper: TestProviders, + }); + + expect(screen.getByTestId('createIndexModalIndexName')).toBeInTheDocument(); + expect(screen.getByTestId('createIndexModalIndexMode')).toBeInTheDocument(); + expect(screen.getByText('Cancel')).toBeInTheDocument(); + expect(screen.getByTestId('createIndexModalCreateButton')).toBeInTheDocument(); + }); + + it('disables create button when index name is empty', () => { + render(, { + wrapper: TestProviders, + }); + + const createButton = screen.getByTestId('createIndexModalCreateButton'); + expect(createButton).toBeDisabled(); + }); + + it('enables create button when index name is not empty', () => { + render(, { + wrapper: TestProviders, + }); + + const input = screen.getByTestId('createIndexModalIndexName'); + fireEvent.change(input, { target: { value: 'my-index' } }); + + const createButton = screen.getByTestId('createIndexModalCreateButton'); + expect(createButton).not.toBeDisabled(); + }); + + it('calls onClose when cancel button is clicked', () => { + render(, { + wrapper: TestProviders, + }); + + fireEvent.click(screen.getByText('Cancel')); + expect(onCloseMock).toHaveBeenCalled(); + }); + + it('calls onCreate and createPrivMonImportIndex with trimmed index name', async () => { + render(, { + wrapper: TestProviders, + }); + + const input = screen.getByTestId('createIndexModalIndexName'); + fireEvent.change(input, { target: { value: ' my-index ' } }); + + const createButton = screen.getByTestId('createIndexModalCreateButton'); + fireEvent.click(createButton); + + await waitFor(() => { + expect(mockCreatePrivMonImportIndex).toHaveBeenCalledWith({ + name: 'my-index', + mode: 'standard', + }); + expect(onCreateMock).toHaveBeenCalledWith('my-index'); + }); + }); + + it('shows error callout if createPrivMonImportIndex throws', async () => { + const errorMsg = 'Something went wrong'; + mockCreatePrivMonImportIndex.mockRejectedValue({ + body: { message: errorMsg }, + }); + + render(, { + wrapper: TestProviders, + }); + + const input = screen.getByTestId('createIndexModalIndexName'); + fireEvent.change(input, { target: { value: 'index' } }); + + const createButton = screen.getByTestId('createIndexModalCreateButton'); + fireEvent.click(createButton); + + await waitFor(() => { + expect(screen.getByText(`Error creating index: ${errorMsg}`)).toBeInTheDocument(); + }); + }); + + it('changes index mode when select is changed', () => { + render(, { + wrapper: TestProviders, + }); + + const select = screen.getByTestId('createIndexModalIndexMode'); + fireEvent.change(select, { target: { value: 'lookup' } }); + expect((select as HTMLSelectElement).value).toBe('lookup'); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/create_index_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/create_index_modal.tsx new file mode 100644 index 000000000000..f4aa8479f6f1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/create_index_modal.tsx @@ -0,0 +1,166 @@ +/* + * 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, { useState, useCallback } from 'react'; +import { + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, + EuiButton, + EuiModalFooter, + EuiSpacer, + EuiModalHeaderTitle, + EuiModalHeader, + EuiModalBody, + EuiModal, + EuiFormRow, + EuiFieldText, + EuiSelect, + EuiCallOut, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import type { SecurityAppError } from '@kbn/securitysolution-t-grid'; +import { useEntityAnalyticsRoutes } from '../../../api/api'; + +enum IndexMode { + STANDARD = 'standard', + LOOKUP = 'lookup', +} + +const INDEX_NAME_LABEL = i18n.translate( + 'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.createIndex.indexNameLabel', + { + defaultMessage: 'Index name', + } +); + +const INDEX_MODE_LABEL = i18n.translate( + 'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.createIndex.indexModeLabel', + { + defaultMessage: 'Index mode', + } +); + +const INDEX_MODES = [ + { + value: IndexMode.STANDARD, + text: i18n.translate( + 'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.createIndex.mode.standard', + { defaultMessage: 'Standard' } + ), + }, + { + value: IndexMode.LOOKUP, + text: i18n.translate( + 'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.createIndex.mode.lookup', + { defaultMessage: 'Lookup' } + ), + }, +]; + +export const CreateIndexModal = ({ + onClose, + onCreate, +}: { + onClose: () => void; + onCreate: (indexName: string) => void; +}) => { + const [indexName, setIndexName] = useState(''); + const [indexMode, setIndexMode] = useState(IndexMode.STANDARD); + const [error, setError] = useState(null); + const { createPrivMonImportIndex } = useEntityAnalyticsRoutes(); + + const handleCreate = useCallback(async () => { + setError(null); + const trimmedName = indexName.trim(); + + await createPrivMonImportIndex({ + name: trimmedName, + mode: indexMode, + }).catch((err: SecurityAppError) => { + setError( + i18n.translate( + 'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.createIndex.error', + { + defaultMessage: 'Error creating index: {error}', + values: { error: err.body.message || err.message || 'Unknown error' }, + } + ) + ); + }); + onCreate(trimmedName); + }, [indexName, createPrivMonImportIndex, indexMode, onCreate]); + + return ( + + + + + + + + {error && ( + <> + {error} + + + )} + + setIndexName(e.target.value)} + aria-label={INDEX_NAME_LABEL} + data-test-subj="createIndexModalIndexName" + /> + + + + setIndexMode(e.target.value as IndexMode)} + aria-label={INDEX_MODE_LABEL} + data-test-subj="createIndexModalIndexMode" + /> + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.test.tsx index 2c099d46ba18..8b11dfabdc8d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.test.tsx @@ -28,29 +28,22 @@ jest.mock('../hooks/use_fetch_privileged_user_indices', () => ({ describe('IndexSelectorModal', () => { const onCloseMock = jest.fn(); + const onImportMock = jest.fn(); afterEach(() => { jest.clearAllMocks(); }); it('renders the modal when isOpen is true', () => { - render(, { + render(, { wrapper: TestProviders, }); expect(screen.getByText('Select index')).toBeInTheDocument(); }); - it('does not render the modal when isOpen is false', () => { - render(, { - wrapper: TestProviders, - }); - - expect(screen.queryByText('Select index')).not.toBeInTheDocument(); - }); - it('calls onClose when the cancel button is clicked', () => { - render(, { + render(, { wrapper: TestProviders, }); @@ -60,7 +53,7 @@ describe('IndexSelectorModal', () => { }); it('displays the indices in the combo box', () => { - render(, { + render(, { wrapper: TestProviders, }); @@ -77,7 +70,7 @@ describe('IndexSelectorModal', () => { error: new Error('Test error'), }); - render(, { + render(, { wrapper: TestProviders, }); @@ -91,7 +84,7 @@ describe('IndexSelectorModal', () => { error: null, }); - render(, { + render(, { wrapper: TestProviders, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.tsx index 1571d6b28df8..f90cbc03b461 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/components/select_index_modal.tsx @@ -4,7 +4,7 @@ * 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 React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiButtonEmpty, @@ -23,9 +23,11 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { useDebounceFn } from '@kbn/react-hooks'; +import { useBoolean, useDebounceFn } from '@kbn/react-hooks'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useFetchPrivilegedUserIndices } from '../hooks/use_fetch_privileged_user_indices'; +import { useEntityAnalyticsRoutes } from '../../../api/api'; +import { CreateIndexModal } from './create_index_modal'; const SELECT_INDEX_LABEL = i18n.translate( 'xpack.securitySolution.entityAnalytics.privilegedUserMonitoring.selectIndex.comboboxPlaceholder', @@ -44,17 +46,20 @@ const LOADING_ERROR_MESSAGE = i18n.translate( const DEBOUNCE_OPTIONS = { wait: 300 }; export const IndexSelectorModal = ({ - isOpen, onClose, + onImport, }: { - isOpen: boolean; onClose: () => void; + onImport: (userCount: number) => void; }) => { + const [isCreateIndexModalOpen, { on: showCreateIndexModal, off: hideCreateIndexModal }] = + useBoolean(false); const { addError } = useAppToasts(); const [searchQuery, setSearchQuery] = useState(undefined); - const { data: indices, isFetching, error } = useFetchPrivilegedUserIndices(searchQuery); + const { data: indices, isFetching, error, refetch } = useFetchPrivilegedUserIndices(searchQuery); const [selectedOptions, setSelected] = useState>>([]); const debouncedSetSearchQuery = useDebounceFn(setSearchQuery, DEBOUNCE_OPTIONS); + const { registerPrivMonMonitoredIndices } = useEntityAnalyticsRoutes(); const options = useMemo( () => indices?.map((index) => ({ @@ -69,11 +74,26 @@ export const IndexSelectorModal = ({ } }, [addError, error]); - if (!isOpen) { - return null; - } + const addPrivilegedUsers = useCallback(async () => { + if (selectedOptions.length > 0) { + await registerPrivMonMonitoredIndices(selectedOptions.map(({ label }) => label).join(',')); - return ( + onImport(0); // The API does not return the user count because it is not available at this point. + } + }, [onImport, registerPrivMonMonitoredIndices, selectedOptions]); + + const onCreateIndex = useCallback( + (indexName: string) => { + hideCreateIndexModal(); + setSelected(selectedOptions.concat({ label: indexName })); + refetch(); + }, + [hideCreateIndexModal, refetch, selectedOptions] + ); + + return isCreateIndexModalOpen ? ( + + ) : ( @@ -124,7 +144,7 @@ export const IndexSelectorModal = ({ - {}}> + - {}} fill> + { const { dataViewSpec } = useDataViewSpec(); const sourcererDataView = newDataViewPickerEnabled ? dataViewSpec : oldSourcererDataView; - + const engineStatus = usePrivilegedMonitoringEngineStatus(); const initEngineCallBack = useCallback( async (userCount: number) => { dispatch({ type: 'INITIALIZING_ENGINE', userCount }); @@ -101,6 +119,33 @@ export const EntityAnalyticsPrivilegedUserMonitoringPage = () => { const onManageUserClicked = useCallback(() => {}, []); + useEffect(() => { + if (engineStatus.isLoading) { + return; + } + + if (engineStatus.isError && engineStatus.error.body.status_code === 404) { + return dispatch({ type: 'SHOW_ONBOARDING' }); + } else { + const errorMessage = engineStatus.error?.body.message ?? engineStatus.data?.error?.message; + + return dispatch({ + type: 'SHOW_DASHBOARD', + onboardingCallout: undefined, + error: errorMessage, + }); + } + }, [ + engineStatus.data?.error?.message, + engineStatus.error?.body, + engineStatus.isError, + engineStatus.isLoading, + ]); + + const fullHeightCSS = css` + min-height: calc(100vh - 240px); + `; + return ( <> {state.type === 'dashboard' && ( @@ -110,11 +155,26 @@ export const EntityAnalyticsPrivilegedUserMonitoringPage = () => { )} + {state.type === 'fetchingEngineStatus' && ( + <> + + } + /> + + + + + + + )} + {state.type === 'onboarding' && ( <> - dispatch({ type: 'SHOW_DASHBOARD' })}> - {'Go to dashboards =>'} - @@ -131,11 +191,7 @@ export const EntityAnalyticsPrivilegedUserMonitoringPage = () => { /> } /> - + {state.initResponse?.status === 'error' ? ( { /> diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts index 5b7fa647cb88..2e2042ac8a04 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/indices.ts @@ -9,6 +9,21 @@ import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; export type MappingProperties = NonNullable; +export const PRIVILEGED_MONITOR_IMPORT_USERS_INDEX_MAPPING: MappingProperties = { + user: { + properties: { + name: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + }, + }, +}; + export const PRIVILEGED_MONITOR_USERS_INDEX_MAPPING: MappingProperties = { 'event.ingested': { type: 'date', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts index 3ce41f5a59df..be1910a4b793 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.ts @@ -40,7 +40,10 @@ import { import type { ApiKeyManager } from './auth/api_key'; import { startPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task'; import { createOrUpdateIndex } from '../utils/create_or_update_index'; -import { generateUserIndexMappings } from './indices'; +import { + PRIVILEGED_MONITOR_IMPORT_USERS_INDEX_MAPPING, + generateUserIndexMappings, +} from './indices'; import { PrivilegeMonitoringEngineDescriptorClient } from './saved_object/privilege_monitoring'; import { POST_EXCLUDE_INDICES, @@ -157,6 +160,15 @@ export class PrivilegeMonitoringDataClient { return descriptor; } + async getEngineStatus() { + const engineDescriptor = await this.engineClient.get(); + + return { + status: engineDescriptor.status, + error: engineDescriptor.error, + }; + } + public async createOrUpdateIndex() { await createOrUpdateIndex({ esClient: this.internalUserClient, @@ -182,17 +194,44 @@ export class PrivilegeMonitoringDataClient { } } + /** + * This create a index for user to populate privileged users. + * It already defines the mappings and settings for the index. + */ + public createPrivilegesImportIndex(indexName: string, mode: 'lookup' | 'standard') { + this.log('info', `Creating privileges import index: ${indexName} with mode: ${mode}`); + // Use the current user client to create the index, the internal user does not have permissions to any index + return this.esClient.indices.create({ + index: indexName, + mappings: { properties: PRIVILEGED_MONITOR_IMPORT_USERS_INDEX_MAPPING }, + settings: { + mode, + }, + }); + } + 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' + fields: ['user.name.keyword'], // search for indices with field 'user.name.keyword' of type 'keyword' include_unmapped: false, ignore_unavailable: true, allow_no_indices: true, expand_wildcards: 'open', include_empty_fields: false, filters: '-parent', + index_filter: { + bool: { + must: [ + { + exists: { + field: 'user.name.keyword', + }, + }, + ], + }, + }, }); if (!Array.isArray(indices) || indices.length === 0) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/create_index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/create_index.ts new file mode 100644 index 000000000000..d4bfcd96eaf3 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/create_index.ts @@ -0,0 +1,62 @@ +/* + * 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 { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { CreatePrivilegesImportIndexRequestBody } from '../../../../../common/api/entity_analytics/monitoring/create_index.gen'; +import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../types'; + +export const createPrivilegeMonitoringIndicesRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + logger: Logger +) => { + router.versioned + .put({ + 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: { + body: buildRouteValidationWithZod(CreatePrivilegesImportIndexRequestBody), + }, + }, + }, + + async (context, request, response): Promise> => { + const secSol = await context.securitySolution; + const siemResponse = buildSiemResponse(response); + const indexName = request.body.name; + const indexMode = request.body.mode; + + try { + await secSol + .getPrivilegeMonitoringDataClient() + .createPrivilegesImportIndex(indexName, indexMode); + + return response.ok(); + } catch (e) { + const error = transformError(e); + logger.error(`Error creating privilege monitoring indices: ${error.message}`); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/health.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/health.ts index 77b0f403c25f..846237dca059 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/health.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/health.ts @@ -36,11 +36,20 @@ export const healthCheckPrivilegeMonitoringRoute = ( async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); + const secSol = await context.securitySolution; try { - return response.ok({ body: { ok: true } }); + const body = await secSol.getPrivilegeMonitoringDataClient().getEngineStatus(); + return response.ok({ body }); } catch (e) { const error = transformError(e); + + if (error?.statusCode === 404) { + return response.ok({ + body: { status: 'not_found' }, + }); + } + logger.error(`Error checking privilege monitoring health: ${error.message}`); return siemResponse.error({ statusCode: error.statusCode, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_privilege_monitoring_routes.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_privilege_monitoring_routes.ts index 17e3f3e2f675..c511c6d86124 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_privilege_monitoring_routes.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/register_privilege_monitoring_routes.ts @@ -6,6 +6,7 @@ */ import type { EntityAnalyticsRoutesDeps } from '../../types'; +import { createPrivilegeMonitoringIndicesRoute } from './create_index'; import { healthCheckPrivilegeMonitoringRoute } from './health'; import { initPrivilegeMonitoringEngineRoute } from './init'; import { monitoringEntitySourceRoute } from './monitoring_entity_source'; @@ -27,11 +28,12 @@ export const registerPrivilegeMonitoringRoutes = ({ logger, config, }: EntityAnalyticsRoutesDeps) => { - initPrivilegeMonitoringEngineRoute(router, logger, config); - healthCheckPrivilegeMonitoringRoute(router, logger, config); padInstallRoute(router, logger, config); padGetStatusRoute(router, logger, config); - searchPrivilegeMonitoringIndicesRoute(router, logger, config); + initPrivilegeMonitoringEngineRoute(router, logger, config); + healthCheckPrivilegeMonitoringRoute(router, logger, config); + searchPrivilegeMonitoringIndicesRoute(router, logger); + createPrivilegeMonitoringIndicesRoute(router, logger); monitoringEntitySourceRoute(router, logger, config); createUserRoute(router, logger); deleteUserRoute(router, logger); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/search_indices.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/search_indices.ts index 8757a687f85b..05a999eea6bb 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/search_indices.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/search_indices.ts @@ -19,8 +19,7 @@ const LIMIT = 20; export const searchPrivilegeMonitoringIndicesRoute = ( router: EntityAnalyticsRoutesDeps['router'], - logger: Logger, - config: EntityAnalyticsRoutesDeps['config'] + logger: Logger ) => { router.versioned .get({ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts index 910cd429f37e..7562b498bd02 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts @@ -18,7 +18,9 @@ interface PrivilegeMonitoringEngineDescriptorDependencies { interface PrivilegedMonitoringEngineDescriptor { status: MonitoringEngineDescriptor['status']; - error?: Record; + error?: Record & { + message?: string; + }; } export class PrivilegeMonitoringEngineDescriptorClient { diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 94956f4848c3..a4a714561d87 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -27,6 +27,7 @@ import { ConfigureRiskEngineSavedObjectRequestBodyInput } from '@kbn/security-so import { CopyTimelineRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/copy_timeline/copy_timeline_route.gen'; import { CreateAlertsMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/create_signals_migration/create_signals_migration.gen'; import { CreateAssetCriticalityRecordRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen'; +import { CreatePrivilegesImportIndexRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/monitoring/create_index.gen'; import { CreatePrivMonUserRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/privilege_monitoring/users/create.gen'; import { CreateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/create_rule/create_rule_route.gen'; import { CreateRuleMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/siem_migrations/model/api/rules/rule_migration.gen'; @@ -332,6 +333,17 @@ If a record already exists for the specified entity, that record is overwritten .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + createPrivilegesImportIndex( + props: CreatePrivilegesImportIndexProps, + kibanaSpace: string = 'default' + ) { + return supertest + .put(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') + .send(props.body as object); + }, createPrivMonUser(props: CreatePrivMonUserProps, kibanaSpace: string = 'default') { return supertest .post(routeWithNamespace('/api/entity_analytics/monitoring/users', kibanaSpace)) @@ -1908,6 +1920,9 @@ export interface CreateAlertsMigrationProps { export interface CreateAssetCriticalityRecordProps { body: CreateAssetCriticalityRecordRequestBodyInput; } +export interface CreatePrivilegesImportIndexProps { + body: CreatePrivilegesImportIndexRequestBodyInput; +} export interface CreatePrivMonUserProps { body: CreatePrivMonUserRequestBodyInput; } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/search_indices.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/search_indices.ts index 211493d78499..58dddea6a314 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/search_indices.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/search_indices.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { PRIVILEGED_MONITOR_IMPORT_USERS_INDEX_MAPPING } from '@kbn/security-solution-plugin/server/lib/entity_analytics/privilege_monitoring/indices'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getService }: FtrProviderContext) => { @@ -24,7 +25,10 @@ export default ({ getService }: FtrProviderContext) => { describe('@ess @serverless @skipInServerlessMKI EntityAnalytics Monitoring SearchIndices', () => { before(async () => { - await es.indices.create({ index: indexName }); + await es.indices.create({ + index: indexName, + mappings: { properties: PRIVILEGED_MONITOR_IMPORT_USERS_INDEX_MAPPING }, + }); }); after(async () => {