diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml index 4e1c209a6da35..edcc1080517b8 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.schema.yaml @@ -73,6 +73,19 @@ paths: "200": description: Entity source deleted successfully + /api/entity_analytics/monitoring/entity_source/list: + get: + operationId: listEntitySources + summary: List all entity source configurations + responses: + "200": + description: List of entity sources retrieved + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/MonitoringEntitySourceDescriptor" components: schemas: MonitoringEntitySourceDescriptor: diff --git a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privilege_monitoring/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privilege_monitoring/constants.ts index 55d8928cfaebd..b051548fedbd1 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privilege_monitoring/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/entity_analytics/privilege_monitoring/constants.ts @@ -14,3 +14,5 @@ export const getPrivilegedMonitorUsersIndex = (namespace: string) => // Not required in phase 0. export const getPrivilegedMonitorGroupsIndex = (namespace: string) => `${privilegedMonitorBaseIndexName}.groups-${namespace}`; +// Default index for privileged monitoring users. Not required. +export const defaultMonitoringUsersIndex = 'entity_analytics.privileged_monitoring'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/privileges.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/privileges.ts index 9e1a2b84a6688..a548112144fe6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/privileges.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/privileges.ts @@ -6,7 +6,7 @@ */ import { PRIVILEGE_MONITORING_INTERNAL_INDICES_PATTERN } from '../constants'; -import { privilegeMonitoringTypeName } from '../saved_object/privilege_monitoring_type'; +import { privilegeMonitoringTypeName } from '../saved_objects'; export const privilegeMonitoringRuntimePrivileges = (sourceIndices: string[]) => ({ cluster: ['manage_ingest_pipelines', 'manage_index_templates'], diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts index f95aa247786ee..db7243443584a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.test.ts @@ -11,7 +11,7 @@ import { elasticsearchServiceMock, loggingSystemMock, } from '@kbn/core/server/mocks'; -import { monitoringEntitySourceTypeName } from './saved_object/monitoring_entity_source_type'; +import { monitoringEntitySourceTypeName } from './saved_objects'; import type { SavedObject, SavedObjectsFindResponse } from '@kbn/core/server'; describe('MonitoringEntitySourceDataClient', () => { @@ -32,6 +32,7 @@ describe('MonitoringEntitySourceDataClient', () => { const testDescriptor = { type: 'test-type', name: 'Test Source', + indexPattern: 'test-index-pattern', matchers: [ { fields: ['user.role'], @@ -49,13 +50,19 @@ describe('MonitoringEntitySourceDataClient', () => { describe('init', () => { it('should initialize Monitoring Entity Source Sync Config Successfully', async () => { + defaultOpts.soClient.update.mockImplementation(() => { + const err = new Error('Not found'); + // Simulate Kibana-style 404 error + (err as Error & { output?: { statusCode: number } }).output = { statusCode: 404 }; + throw err; + }); defaultOpts.soClient.find.mockResolvedValue({ total: 0, saved_objects: [], } as unknown as SavedObjectsFindResponse); defaultOpts.soClient.create.mockResolvedValue({ - id: `entity-analytics-monitoring-entity-source-${namespace}`, + id: 'temp-id', // TODO: update to use dynamic ID type: monitoringEntitySourceTypeName, attributes: testDescriptor, references: [], @@ -66,7 +73,9 @@ describe('MonitoringEntitySourceDataClient', () => { expect(defaultOpts.soClient.create).toHaveBeenCalledWith( monitoringEntitySourceTypeName, testDescriptor, - { id: `entity-analytics-monitoring-entity-source-${namespace}` } + { + id: `entity-analytics-monitoring-entity-source-${namespace}-${testDescriptor.type}-${testDescriptor.indexPattern}`, + } ); expect(result).toEqual(testDescriptor); @@ -76,7 +85,6 @@ describe('MonitoringEntitySourceDataClient', () => { describe('get', () => { it('should get Monitoring Entity Source Sync Config Successfully', async () => { const getResponse = { - id: `entity-analytics-monitoring-entity-source-${namespace}`, type: monitoringEntitySourceTypeName, attributes: testDescriptor, references: [], @@ -85,7 +93,7 @@ describe('MonitoringEntitySourceDataClient', () => { const result = await dataClient.get(); expect(defaultOpts.soClient.get).toHaveBeenCalledWith( monitoringEntitySourceTypeName, - `entity-analytics-monitoring-entity-source-${namespace}` + `temp-id` // TODO: https://github.com/elastic/security-team/issues/12851 ); expect(result).toEqual(getResponse.attributes); }); @@ -98,12 +106,25 @@ describe('MonitoringEntitySourceDataClient', () => { saved_objects: [{ attributes: testDescriptor }], } as unknown as SavedObjectsFindResponse; + const testSourceObject = { + filter: {}, + indexPattern: 'test-index-pattern', + matchers: [ + { + fields: ['user.role'], + values: ['admin'], + }, + ], + name: 'Test Source', + type: 'test-type', + }; + defaultOpts.soClient.find.mockResolvedValue( existingDescriptor as unknown as SavedObjectsFindResponse ); defaultOpts.soClient.update.mockResolvedValue({ - id: `entity-analytics-monitoring-entity-source-${namespace}`, + id: `temp-id`, // TODO: https://github.com/elastic/security-team/issues/12851 type: monitoringEntitySourceTypeName, attributes: { ...testDescriptor, name: 'Updated Source' }, references: [], @@ -114,8 +135,8 @@ describe('MonitoringEntitySourceDataClient', () => { expect(defaultOpts.soClient.update).toHaveBeenCalledWith( monitoringEntitySourceTypeName, - `entity-analytics-monitoring-entity-source-${namespace}`, - testDescriptor, + `entity-analytics-monitoring-entity-source-${namespace}-${testDescriptor.type}-${testDescriptor.indexPattern}`, + testSourceObject, { refresh: 'wait_for' } ); @@ -128,7 +149,7 @@ describe('MonitoringEntitySourceDataClient', () => { await dataClient.delete(); expect(mockSavedObjectClient.delete).toHaveBeenCalledWith( monitoringEntitySourceTypeName, - `entity-analytics-monitoring-entity-source-${namespace}` + `temp-id` // TODO: https://github.com/elastic/security-team/issues/12851 ); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts index 1b106825c1ca2..75d80e4f93368 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/monitoring_entity_source_data_client.ts @@ -10,7 +10,7 @@ import type { MonitoringEntitySourceDescriptor, MonitoringEntitySourceResponse, } from '../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; -import { MonitoringEntitySourceDescriptorClient } from './saved_object/monitoring_entity_source'; +import { MonitoringEntitySourceDescriptorClient } from './saved_objects'; interface MonitoringEntitySourceDataClientOpts { logger: Logger; @@ -31,7 +31,9 @@ export class MonitoringEntitySourceDataClient { public async init( input: MonitoringEntitySourceDescriptor ): Promise { - const descriptor = await this.monitoringEntitySourceClient.create(input); + const descriptor = await this.monitoringEntitySourceClient.create({ + ...input, + }); this.log('debug', 'Initializing MonitoringEntitySourceDataClient Saved Object'); return descriptor; } @@ -46,7 +48,7 @@ export class MonitoringEntitySourceDataClient { const sanitizedUpdate = { ...update, - matchers: update.matchers?.map((matcher) => ({ + matchers: update.matchers?.map((matcher: { fields: string[]; values: string[] }) => ({ fields: matcher.fields ?? [], values: matcher.values ?? [], })), @@ -60,6 +62,11 @@ export class MonitoringEntitySourceDataClient { return this.monitoringEntitySourceClient.delete(); } + public async list(): Promise { + this.log('debug', 'Finding all Monitoring Entity Source Sync saved objects'); + return this.monitoringEntitySourceClient.findAll(); + } + private log(level: Exclude, msg: string) { this.opts.logger[level]( `[Monitoring Entity Source Sync][namespace: ${this.opts.namespace}] ${msg}` diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts index e25c57c7c91ee..63c0f075640c2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/privilege_monitoring_data_client.test.ts @@ -24,17 +24,23 @@ jest.mock('./tasks/privilege_monitoring_task', () => { }; }); -jest.mock('./saved_object/privilege_monitoring', () => { +jest.mock('./saved_objects', () => { return { + MonitoringEntitySourceDescriptorClient: jest.fn().mockImplementation(() => ({ + findByIndex: jest.fn().mockResolvedValue([]), + create: jest.fn(), + })), PrivilegeMonitoringEngineDescriptorClient: jest.fn().mockImplementation(() => ({ init: jest.fn().mockResolvedValue({ status: 'success' }), update: jest.fn(), })), }; }); + describe('Privilege Monitoring Data Client', () => { const mockSavedObjectClient = savedObjectsClientMock.create(); const clusterClientMock = elasticsearchServiceMock.createScopedClusterClient(); + const esClientMock = clusterClientMock.asCurrentUser; const loggerMock = loggingSystemMock.createLogger(); const auditMock = { log: jest.fn().mockReturnValue(undefined) }; loggerMock.debug = jest.fn(); @@ -66,7 +72,7 @@ describe('Privilege Monitoring Data Client', () => { expect(mockCreateOrUpdateIndex).toHaveBeenCalled(); expect(mockStartPrivilegeMonitoringTask).toHaveBeenCalled(); - expect(loggerMock.debug).toHaveBeenCalledTimes(1); + expect(loggerMock.debug).toHaveBeenCalledTimes(3); expect(auditMock.log).toHaveBeenCalled(); expect(result).toEqual(mockDescriptor); }); @@ -137,4 +143,124 @@ describe('Privilege Monitoring Data Client', () => { // TODO: implement once we have more auditing }); }); + + describe('syncAllIndexUsers', () => { + const mockLog = jest.fn(); + + it('should sync all index users successfully', async () => { + const mockMonitoringSOSources = [ + { name: 'source1', indexPattern: 'index1' }, + { name: 'source2', indexPattern: 'index2' }, + ]; + const findByIndexMock = jest.fn().mockResolvedValue(mockMonitoringSOSources); + Object.defineProperty(dataClient, 'monitoringIndexSourceClient', { + value: { + init: jest.fn().mockResolvedValue({ status: 'success' }), + update: jest.fn(), + findByIndex: findByIndexMock, + }, + }); + dataClient.syncUsernamesFromIndex = jest.fn().mockResolvedValue(['user1', 'user2']); + await dataClient.plainIndexSync(); + expect(findByIndexMock).toHaveBeenCalled(); + expect(dataClient.syncUsernamesFromIndex).toHaveBeenCalledTimes(2); + expect(dataClient.syncUsernamesFromIndex).toHaveBeenCalledWith({ + indexName: 'index1', + kuery: undefined, + }); + }); + + it('logs and returns if no index sources', async () => { + Object.defineProperty(dataClient, 'log', { value: mockLog }); + const findByIndexMock = jest.fn().mockResolvedValue([]); + Object.defineProperty(dataClient, 'monitoringIndexSourceClient', { + value: { + init: jest.fn().mockResolvedValue({ status: 'success' }), + update: jest.fn(), + findByIndex: findByIndexMock, + }, + }); + + await dataClient.plainIndexSync(); + + expect(mockLog).toHaveBeenCalledWith( + 'debug', + expect.stringContaining('No monitoring index sources found. Skipping sync.') + ); + }); + + it('skips sources without indexPattern', async () => { + Object.defineProperty(dataClient, 'monitoringIndexSourceClient', { + value: { + findByIndex: jest.fn().mockResolvedValue([ + { name: 'no-index', indexPattern: undefined }, + { name: 'with-index', indexPattern: 'foo' }, + ]), + init: jest.fn().mockResolvedValue({ status: 'success' }), + update: jest.fn(), + }, + }); + + dataClient.syncUsernamesFromIndex = jest.fn().mockResolvedValue(['user1']); + Object.defineProperty(dataClient, 'findStaleUsersForIndex', { + value: jest.fn().mockResolvedValue([]), + }); + await dataClient.plainIndexSync(); + // Should only be called for the source with indexPattern + expect(dataClient.syncUsernamesFromIndex).toHaveBeenCalledTimes(1); + expect(dataClient.syncUsernamesFromIndex).toHaveBeenCalledWith({ + indexName: 'foo', + kuery: undefined, + }); + }); + + it('should retrieve all usernames from index and perform bulk ops', async () => { + const mockHits = [ + { + _source: { user: { name: 'frodo' } }, + _id: '1', + sort: [1], + }, + { + _source: { user: { name: 'samwise' } }, + _id: '2', + sort: [2], + }, + ]; + + const mockMonitoredUserHits = { + hits: { + hits: [ + { + _source: { user: { name: 'frodo' } }, + _id: '1', + }, + { + _source: { user: { name: 'samwise' } }, + _id: '2', + }, + ], + }, + }; + + dataClient.searchUsernamesInIndex = jest + .fn() + .mockResolvedValueOnce({ hits: { hits: mockHits } }) // first batch + .mockResolvedValueOnce({ hits: { hits: [] } }); // second batch = end + + dataClient.getMonitoredUsers = jest.fn().mockResolvedValue(mockMonitoredUserHits); + dataClient.buildBulkOperationsForUsers = jest.fn().mockReturnValue([{ index: { _id: '1' } }]); + dataClient.getIndex = jest.fn().mockReturnValue('test-index'); + + const usernames = await dataClient.syncUsernamesFromIndex({ + indexName: 'test-index', + }); + + expect(usernames).toEqual(['frodo', 'samwise']); + expect(dataClient.searchUsernamesInIndex).toHaveBeenCalledTimes(2); + expect(esClientMock.bulk).toHaveBeenCalled(); + expect(dataClient.getMonitoredUsers).toHaveBeenCalledWith(['frodo', 'samwise']); + expect(dataClient.buildBulkOperationsForUsers).toHaveBeenCalled(); + }); + }); }); 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 be1910a4b7934..dfaf549fe856a 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 @@ -22,9 +22,13 @@ import { merge } from 'lodash'; import Papa from 'papaparse'; import { Readable } from 'stream'; -import { getPrivilegedMonitorUsersIndex } from '../../../../common/entity_analytics/privilege_monitoring/constants'; +import type { SortResults } from '@elastic/elasticsearch/lib/api/types'; import type { PrivmonBulkUploadUsersCSVResponse } from '../../../../common/api/entity_analytics/privilege_monitoring/users/upload_csv.gen'; import type { HapiReadableStream } from '../../../types'; +import { + defaultMonitoringUsersIndex, + getPrivilegedMonitorUsersIndex, +} from '../../../../common/entity_analytics/privilege_monitoring/constants'; import type { UpdatePrivMonUserRequestBody } from '../../../../common/api/entity_analytics/privilege_monitoring/users/update.gen'; import type { @@ -44,7 +48,6 @@ import { PRIVILEGED_MONITOR_IMPORT_USERS_INDEX_MAPPING, generateUserIndexMappings, } from './indices'; -import { PrivilegeMonitoringEngineDescriptorClient } from './saved_object/privilege_monitoring'; import { POST_EXCLUDE_INDICES, PRE_EXCLUDE_INDICES, @@ -56,7 +59,6 @@ import { PRIVMON_ENGINE_INITIALIZATION_EVENT, PRIVMON_ENGINE_RESOURCE_INIT_FAILURE_EVENT, } from '../../telemetry/event_based/events'; -import type { PrivMonUserSource } from './types'; import { batchPartitions } from '../shared/streams/batching'; import { queryExistingUsers } from './users/query_existing_users'; @@ -66,6 +68,12 @@ import { softDeleteOmittedUsers } from './users/soft_delete_omitted_users'; import { privilegedUserParserTransform } from './users/privileged_user_parse_transform'; import type { Accumulator } from './users/bulk/utils'; import { accumulateUpsertResults } from './users/bulk/utils'; +import type { PrivMonBulkUser, PrivMonUserSource } from './types'; +import type { MonitoringEntitySourceDescriptor } from './saved_objects'; +import { + PrivilegeMonitoringEngineDescriptorClient, + MonitoringEntitySourceDescriptorClient, +} from './saved_objects'; interface PrivilegeMonitoringClientOpts { logger: Logger; @@ -84,6 +92,7 @@ export class PrivilegeMonitoringDataClient { private esClient: ElasticsearchClient; private internalUserClient: ElasticsearchClient; private engineClient: PrivilegeMonitoringEngineDescriptorClient; + private monitoringIndexSourceClient: MonitoringEntitySourceDescriptorClient; constructor(private readonly opts: PrivilegeMonitoringClientOpts) { this.esClient = opts.clusterClient.asCurrentUser; @@ -93,6 +102,10 @@ export class PrivilegeMonitoringDataClient { soClient: opts.soClient, namespace: opts.namespace, }); + this.monitoringIndexSourceClient = new MonitoringEntitySourceDescriptorClient({ + soClient: opts.soClient, + namespace: opts.namespace, + }); } async init(): Promise { @@ -109,7 +122,17 @@ export class PrivilegeMonitoringDataClient { const descriptor = await this.engineClient.init(); this.log('debug', `Initialized privileged monitoring engine saved object`); - + // create default index source for privilege monitoring + const indexSourceDescriptor = await this.monitoringIndexSourceClient.create({ + type: 'index', + managed: true, + indexPattern: defaultMonitoringUsersIndex, + name: 'default-monitoring-index', + }); + this.log( + 'debug', + `Created index source for privilege monitoring: ${JSON.stringify(indexSourceDescriptor)}` + ); try { await this.createOrUpdateIndex().catch((e) => { if (e.meta.body.error.type === 'resource_already_exists_exception') { @@ -134,6 +157,8 @@ export class PrivilegeMonitoringDataClient { this.opts.telemetry?.reportEvent(PRIVMON_ENGINE_INITIALIZATION_EVENT.eventType, { duration, }); + // sync all index users from monitoring sources + await this.plainIndexSync(); } catch (e) { this.log('error', `Error initializing privilege monitoring engine: ${e}`); this.audit( @@ -394,4 +419,310 @@ export class PrivilegeMonitoringDataClient { return this.opts.auditLogger?.log(event); } + + /** + * Synchronizes users from monitoring index sources and soft-deletes (mark as not privileged) stale entries. + * + * This method: + * - Retrieves all saved objects of type 'index' that define monitoring sources. + * - For each valid source with an index pattern, fetches usernames from the monitoring index. + * - Identifies users no longer present in the source index (stale users). + * - Performs a bulk soft-delete (marks as not privileged) for all stale users found. + * - Handles missing indices gracefully by logging a warning and skipping them. + * + * Additionally, all users from index sources are synced with the internal privileged user index, + * ensuring each user is either created or updated with the latest data. + * + * @returns {Promise} Resolves when synchronization and soft-deletion are complete. + */ + public async plainIndexSync() { + // get all monitoring index source saved objects of type 'index' + const indexSources: MonitoringEntitySourceDescriptor[] = + await this.monitoringIndexSourceClient.findByIndex(); + if (indexSources.length === 0) { + this.log('debug', 'No monitoring index sources found. Skipping sync.'); + return; + } + const allStaleUsers: PrivMonBulkUser[] = []; + + for (const source of indexSources) { + // eslint-disable-next-line no-continue + if (!source.indexPattern) continue; // if no index pattern, skip this source + const index: string = source.indexPattern; + + try { + const batchUserNames = await this.syncUsernamesFromIndex({ + indexName: index, + kuery: source.filter?.kuery, + }); + // collect stale users + const staleUsers = await this.findStaleUsersForIndex(index, batchUserNames); + allStaleUsers.push(...staleUsers); + } catch (error) { + if ( + error?.meta?.body?.error?.type === 'index_not_found_exception' || + error?.message?.includes('index_not_found_exception') + ) { + this.log('warn', `Index "${index}" not found — skipping.`); + // eslint-disable-next-line no-continue + continue; + } + this.log('error', `Unexpected error during sync for index "${index}": ${error.message}`); + } + } + // Soft delete stale users + this.log('debug', `Found ${allStaleUsers.length} stale users across all index sources.`); + if (allStaleUsers.length > 0) { + const ops = this.bulkOperationsForSoftDeleteUsers(allStaleUsers, this.getIndex()); + await this.esClient.bulk({ body: ops }); + } + } + + /** + * Synchronizes usernames from a specified index by collecting them in batches + * and performing create or update operations in the privileged user index. + * + * This method: + * - Executes a paginated search on the provided index (with optional KQL filter). + * - Extracts `user.name` values from each document. + * - Checks for existing monitored users to determine if each username should be created or updated. + * - Performs bulk operations to insert or update users in the internal privileged user index. + * + * Designed to support large indices through pagination (`search_after`) and batching. + * Logs each step and handles errors during bulk writes. + * + * @param indexName - Name of the Elasticsearch index to pull usernames from. + * @param kuery - Optional KQL filter to narrow down results. + * @returns A list of all usernames processed from the source index. + */ + public async syncUsernamesFromIndex({ + indexName, + kuery, + }: { + indexName: string; + kuery?: string | unknown; + }): Promise { + const batchUsernames: string[] = []; + let searchAfter: SortResults | undefined; + const batchSize = 100; + + const query = kuery ? toElasticsearchQuery(fromKueryExpression(kuery)) : { match_all: {} }; + while (true) { + const response = await this.searchUsernamesInIndex({ + indexName, + batchSize, + searchAfter, + query, + }); + + const hits = response.hits.hits; + if (hits.length === 0) break; + + // Collect usernames from the hits + for (const hit of hits) { + const username = hit._source?.user?.name; + if (username) batchUsernames.push(username); + } + + const existingUserRes = await this.getMonitoredUsers(batchUsernames); + + const existingUserMap = new Map(); + for (const hit of existingUserRes.hits.hits) { + const username = hit._source?.user?.name; + this.log('debug', `Found existing user: ${username} with ID: ${hit._id}`); + if (username) existingUserMap.set(username, hit._id); + } + + const usersToWrite: PrivMonBulkUser[] = batchUsernames.map((username) => ({ + username, + indexName, + existingUserId: existingUserMap.get(username), + })); + + const ops = this.buildBulkOperationsForUsers(usersToWrite, this.getIndex()); + this.log('debug', `Executing bulk operations for ${usersToWrite.length} users`); + try { + this.log('debug', `Bulk ops preview:\n${JSON.stringify(ops, null, 2)}`); + await this.esClient.bulk({ body: ops }); + } catch (error) { + this.log('error', `Error executing bulk operations: ${error}`); + } + searchAfter = hits[hits.length - 1].sort; + } + return batchUsernames; + } + + private async findStaleUsersForIndex( + indexName: string, + userNames: string[] + ): Promise { + const response = await this.esClient.search({ + index: this.getIndex(), + size: 10, // check this + _source: ['user.name', 'labels.source_indices'], + query: { + bool: { + must: [ + { term: { 'user.is_privileged': true } }, + { term: { 'labels.source_indices.keyword': indexName } }, + ], + must_not: { + terms: { 'user.name': userNames }, + }, + }, + }, + }); + + return response.hits.hits.map((hit) => ({ + username: hit._source?.user?.name ?? 'unknown', + existingUserId: hit._id, + indexName, + })); + } + + public async getMonitoredUsers(batchUsernames: string[]) { + return this.esClient.search({ + index: this.getIndex(), + size: batchUsernames.length, + query: { + bool: { + must: [{ terms: { 'user.name': batchUsernames } }], + }, + }, + }); + } + + /** + * Builds a list of Elasticsearch bulk operations to upsert privileged users. + * + * For each user: + * - If the user already exists (has an ID), generates an `update` operation using a Painless script + * to append the index name to `labels.source_indices` and ensure `'index'` is listed in `labels.sources`. + * - If the user is new, generates an `index` operation to create a new document with default labels. + * + * Logs key steps during operation generation and returns the bulk operations array, ready for submission to the ES Bulk API. + * + * @param users - List of users to create or update. + * @param userIndexName - Name of the Elasticsearch index where user documents are stored. + * @returns An array of bulk operations suitable for the Elasticsearch Bulk API. + */ + public buildBulkOperationsForUsers(users: PrivMonBulkUser[], userIndexName: string): object[] { + const ops: object[] = []; + this.log('info', `Building bulk operations for ${users.length} users`); + for (const user of users) { + if (user.existingUserId) { + // Update user with painless script + this.log( + 'info', + `Updating existing user: ${user.username} with ID: ${user.existingUserId}` + ); + ops.push( + { update: { _index: userIndexName, _id: user.existingUserId } }, + { + script: { + source: ` + if (!ctx._source.labels.source_indices.contains(params.index)) { + ctx._source.labels.source_indices.add(params.index); + } + if (!ctx._source.labels.sources.contains("index")) { + ctx._source.labels.sources.add("index"); + } + `, + params: { + index: user.indexName, + }, + }, + } + ); + } else { + // New user — create + this.log('info', `Creating new user: ${user.username} with index: ${user.indexName}`); + ops.push( + { index: { _index: userIndexName } }, + { + user: { name: user.username, is_privileged: true }, + labels: { + sources: ['index'], + source_indices: [user.indexName], + }, + } + ); + } + } + this.log('info', `Built ${ops.length} bulk operations for users`); + return ops; + } + + /** + * Builds bulk operations to soft-delete users by updating their privilege status. + * + * For each user: + * - Removes the specified `index` from `labels.source_indices`. + * - If no source indices remain, removes `'index'` from `labels.sources`. + * - If no sources remain, sets `user.is_privileged` to `false`, effectively marking the user as no longer privileged. + * + * These operations are used to clean up users that are no longer found in the associated index sources + * without deleting their documents entirely. + * + * @param users - Users to be soft-deleted based on missing index source association. + * @param userIndexName - The Elasticsearch index where user documents are stored. + * @returns An array of bulk update operations compatible with the Elasticsearch Bulk API. + */ + public bulkOperationsForSoftDeleteUsers( + users: PrivMonBulkUser[], + userIndexName: string + ): object[] { + const ops: object[] = []; + this.log('info', `Building bulk operations for soft delete users`); + for (const user of users) { + ops.push( + { update: { _index: userIndexName, _id: user.existingUserId } }, + { + script: { + source: ` + if (ctx._source.labels?.source_indices != null) { + ctx._source.labels.source_indices.removeIf(idx -> idx == params.index); + } + + if (ctx._source.labels?.source_indices == null || ctx._source.labels.source_indices.isEmpty()) { + if (ctx._source.labels?.sources != null) { + ctx._source.labels.sources.removeIf(src -> src == 'index'); + } + } + + if (ctx._source.labels?.sources == null || ctx._source.labels.sources.isEmpty()) { + ctx._source.user.is_privileged = false; + } + `, + params: { + index: user.indexName, + }, + }, + } + ); + } + + return ops; + } + + async searchUsernamesInIndex({ + indexName, + batchSize, + searchAfter, + query, + }: { + indexName: string; + batchSize: number; + searchAfter?: SortResults; + query: object; + }) { + return this.esClient.search<{ user?: { name?: string } }>({ + index: indexName, + size: batchSize, + _source: ['user.name'], + sort: [{ 'user.name.keyword': 'asc' }], + search_after: searchAfter, + query, + }); + } } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/index.ts new file mode 100644 index 0000000000000..f695af1141936 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/index.ts @@ -0,0 +1,9 @@ +/* + * 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 './monitoring_entity_source'; +export * from './list'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/list.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/list.ts new file mode 100644 index 0000000000000..46c4dd3b85383 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/list.ts @@ -0,0 +1,58 @@ +/* + * 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 { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import type { MonitoringEntitySourceResponse } from '../../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; +import { API_VERSIONS, APP_ID } from '../../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../../types'; + +export const listMonitoringEntitySourceRoute = ( + router: EntityAnalyticsRoutesDeps['router'], + logger: Logger, + config: EntityAnalyticsRoutesDeps['config'] +) => { + router.versioned + .get({ + access: 'public', + path: '/api/entity_analytics/monitoring/entity_source/list', + security: { + authz: { + requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: {}, + }, + async ( + context, + request, + response + ): Promise> => { + const siemResponse = buildSiemResponse(response); + + try { + const secSol = await context.securitySolution; + const client = secSol.getMonitoringEntitySourceDataClient(); + const body = await client.list(); + + return response.ok({ body }); + } catch (e) { + const error = transformError(e); + logger.error(`Error listing monitoring entity sources: ${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/monitoring_entity_source.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts similarity index 92% rename from x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts index 239f7a09a9ea2..c6f5f252b997d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/monitoring_entity_source.ts @@ -11,10 +11,10 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { IKibanaResponse, Logger } from '@kbn/core/server'; -import type { MonitoringEntitySourceResponse } from '../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; -import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; -import type { EntityAnalyticsRoutesDeps } from '../../types'; -import { MonitoringEntitySourceDescriptor } from '../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; +import type { MonitoringEntitySourceResponse } from '../../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; +import { API_VERSIONS, APP_ID } from '../../../../../../common/constants'; +import type { EntityAnalyticsRoutesDeps } from '../../../types'; +import { MonitoringEntitySourceDescriptor } from '../../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; export const monitoringEntitySourceRoute = ( router: EntityAnalyticsRoutesDeps['router'], 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 c511c6d861247..c6f5f7e6cf2c4 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 @@ -9,7 +9,10 @@ import type { EntityAnalyticsRoutesDeps } from '../../types'; import { createPrivilegeMonitoringIndicesRoute } from './create_index'; import { healthCheckPrivilegeMonitoringRoute } from './health'; import { initPrivilegeMonitoringEngineRoute } from './init'; -import { monitoringEntitySourceRoute } from './monitoring_entity_source'; +import { + monitoringEntitySourceRoute, + listMonitoringEntitySourceRoute, +} from './monitoring_entity_source'; import { searchPrivilegeMonitoringIndicesRoute } from './search_indices'; import { @@ -35,6 +38,7 @@ export const registerPrivilegeMonitoringRoutes = ({ searchPrivilegeMonitoringIndicesRoute(router, logger); createPrivilegeMonitoringIndicesRoute(router, logger); monitoringEntitySourceRoute(router, logger, config); + listMonitoringEntitySourceRoute(router, logger, config); createUserRoute(router, logger); deleteUserRoute(router, logger); listUsersRoute(router, logger); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/monitoring_entity_source.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/monitoring_entity_source.ts deleted file mode 100644 index 70555be470419..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/monitoring_entity_source.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 { SavedObjectsClientContract } from '@kbn/core/server'; -import { monitoringEntitySourceTypeName } from './monitoring_entity_source_type'; - -export interface MonitoringEntitySourceDependencies { - soClient: SavedObjectsClientContract; - namespace: string; -} - -export interface MonitoringEntitySourceDescriptor { - type: string; - name: string; - managed?: boolean; - indexPattern?: string; - enabled?: boolean; - error?: string; - integrationName?: string; - matchers?: Array<{ - fields: string[]; - values: string[]; - }>; - filter?: Record; -} - -export class MonitoringEntitySourceDescriptorClient { - constructor(private readonly dependencies: MonitoringEntitySourceDependencies) {} - - getSavedObjectId() { - return `entity-analytics-monitoring-entity-source-${this.dependencies.namespace}`; - } - - async create(attributes: MonitoringEntitySourceDescriptor) { - const entitySourceDescriptor = await this.find(); - - if (entitySourceDescriptor.total === 1) { - const { attributes: updated } = - await this.dependencies.soClient.update( - monitoringEntitySourceTypeName, - this.getSavedObjectId(), - attributes, - { refresh: 'wait_for' } - ); - return updated; - } - - const { attributes: created } = - await this.dependencies.soClient.create( - monitoringEntitySourceTypeName, - attributes, - { id: this.getSavedObjectId() } - ); - - return created; - } - - async update(monitoringEntitySource: Partial) { - const id = this.getSavedObjectId(); - const { attributes } = - await this.dependencies.soClient.update( - monitoringEntitySourceTypeName, - id, - monitoringEntitySource, - { refresh: 'wait_for' } - ); - return attributes; - } - - async find() { - return this.dependencies.soClient.find({ - type: monitoringEntitySourceTypeName, - namespaces: [this.dependencies.namespace], - }); - } - - async get() { - const id = this.getSavedObjectId(); - const { attributes } = await this.dependencies.soClient.get( - monitoringEntitySourceTypeName, - id - ); - return attributes; - } - - async delete() { - const id = this.getSavedObjectId(); - await this.dependencies.soClient.delete(monitoringEntitySourceTypeName, id); - } -} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/index.ts new file mode 100644 index 0000000000000..f1f65a6f60d2a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/index.ts @@ -0,0 +1,11 @@ +/* + * 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 './privilege_monitoring'; +export * from './privilege_monitoring_type'; +export * from './monitoring_entity_source_type'; +export * from './monitoring_entity_source'; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts new file mode 100644 index 0000000000000..8725361603b18 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source.ts @@ -0,0 +1,137 @@ +/* + * 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 { SavedObjectsClientContract } from '@kbn/core/server'; +import { monitoringEntitySourceTypeName } from './monitoring_entity_source_type'; + +export interface MonitoringEntitySourceDependencies { + soClient: SavedObjectsClientContract; + namespace: string; +} + +export interface MonitoringEntitySourceDescriptor { + type: string; + name: string; + managed?: boolean; + indexPattern?: string; + enabled?: boolean; + error?: string; + integrationName?: string; + matchers?: Array<{ + fields: string[]; + values: string[]; + }>; + filter?: Record; +} + +export class MonitoringEntitySourceDescriptorClient { + constructor(private readonly dependencies: MonitoringEntitySourceDependencies) {} + + getDynamicSavedObjectId(attributes: MonitoringEntitySourceDescriptor) { + const { type, indexPattern, integrationName } = this.assertValidIdFields(attributes); + const sourceName = indexPattern || integrationName; + return `entity-analytics-monitoring-entity-source-${this.dependencies.namespace}-${type}${ + sourceName ? `-${sourceName}` : '' + }`; + } + + async create(attributes: MonitoringEntitySourceDescriptor) { + const savedObjectId = this.getDynamicSavedObjectId(attributes); + + try { + // If exists, update it. + const { attributes: updated } = + await this.dependencies.soClient.update( + monitoringEntitySourceTypeName, + savedObjectId, + attributes, + { refresh: 'wait_for' } + ); + return updated; + } catch (e) { + if (e.output?.statusCode !== 404) throw e; + + // Does not exist, create it. + const { attributes: created } = + await this.dependencies.soClient.create( + monitoringEntitySourceTypeName, + attributes, + { id: savedObjectId } + ); + return created; + } + } + + async update(monitoringEntitySource: Partial) { + const id = this.getDynamicSavedObjectId( + monitoringEntitySource as MonitoringEntitySourceDescriptor + ); + const { attributes } = + await this.dependencies.soClient.update( + monitoringEntitySourceTypeName, + id, + monitoringEntitySource, + { refresh: 'wait_for' } + ); + return attributes; + } + + async find() { + return this.dependencies.soClient.find({ + type: monitoringEntitySourceTypeName, + namespaces: [this.dependencies.namespace], + }); + } + + /** + * Need to update to understand the id based on the + * type and indexPattern or integrationName. + * + * Two options: create a getById method that takes the id, + * or use a dynamic ID based on the type and indexPattern/integrationName. + */ + async get() { + const { attributes } = await this.dependencies.soClient.get( + monitoringEntitySourceTypeName, + 'temp-id' // TODO: https://github.com/elastic/security-team/issues/12851 + ); + return attributes; + } + + /** + * Need to update to understand the id based on the + * type and indexPattern or integrationName. + * + * * Two options: create a getById method that takes the id, + * or use a dynamic ID based on the type and indexPattern/integrationName. + */ + async delete() { + await this.dependencies.soClient.delete(monitoringEntitySourceTypeName, 'temp-id'); // TODO: https://github.com/elastic/security-team/issues/12851 + } + + public async findByIndex(): Promise { + const result = await this.find(); + return result.saved_objects + .filter((so) => so.attributes.type === 'index') + .map((so) => so.attributes); + } + + public async findAll(): Promise { + const result = await this.find(); + return result.saved_objects + .filter((so) => so.attributes.type !== 'csv') // from the spec we are not using CSV on monitoring + .map((so) => so.attributes); + } + + public assertValidIdFields( + source: Partial + ): MonitoringEntitySourceDescriptor { + if (!source.type || (!source.indexPattern && !source.integrationName)) { + throw new Error('Missing required fields for ID generation'); + } + return source as MonitoringEntitySourceDescriptor; + } +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/monitoring_entity_source_type.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source_type.ts similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/monitoring_entity_source_type.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/monitoring_entity_source_type.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/privilege_monitoring.test.ts similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.test.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/privilege_monitoring.test.ts 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_objects/privilege_monitoring.ts similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/privilege_monitoring.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/privilege_monitoring_type.ts similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type.ts rename to x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/saved_objects/privilege_monitoring_type.ts diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/types.ts index 2d830aee33685..5271edce0a1ec 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/types.ts @@ -6,3 +6,9 @@ */ export type PrivMonUserSource = 'csv' | 'api' | 'index_sync'; + +export interface PrivMonBulkUser { + username: string; + indexName: string; + existingUserId?: string; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts b/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts index 75bf5c8fc9087..79e7bbaaa086c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts @@ -18,8 +18,10 @@ import { type as signalsMigrationType } from './lib/detection_engine/migrations/ import { manifestType, unifiedManifestType } from './endpoint/lib/artifacts/saved_object_mappings'; import { riskEngineConfigurationType } from './lib/entity_analytics/risk_engine/saved_object'; import { entityEngineDescriptorType } from './lib/entity_analytics/entity_store/saved_object'; -import { privilegeMonitoringType } from './lib/entity_analytics/privilege_monitoring/saved_object/privilege_monitoring_type'; -import { monitoringEntitySourceType } from './lib/entity_analytics/privilege_monitoring/saved_object/monitoring_entity_source_type'; +import { + privilegeMonitoringType, + monitoringEntitySourceType, +} from './lib/entity_analytics/privilege_monitoring/saved_objects'; const types = [ noteType,