diff --git a/x-pack/solutions/security/plugins/security_solution/server/config.ts b/x-pack/solutions/security/plugins/security_solution/server/config.ts index 8497abb6d131a..3ba9cbc9d239a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/config.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/config.ts @@ -184,6 +184,9 @@ export const configSchema = schema.object({ }), monitoring: schema.object({ privileges: schema.object({ + developer: schema.object({ + syncInterval: schema.number({ defaultValue: 600 }), // 10 minutes in seconds + }), users: schema.object({ csvUpload: schema.object({ errorRetries: schema.number({ defaultValue: 1 }), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.test.ts new file mode 100644 index 0000000000000..152f1344c3155 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/auth/api_key.test.ts @@ -0,0 +1,36 @@ +/* + * 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. + */ + +describe('Privilege Monitoring API Key Manager', () => { + describe('generate', () => { + it('should create and store an API key', async () => { + // TODO: Implement this test for creating and storing an API key + }); + it('should throw if encryptedSavedObjects is missing', async () => { + // TODO: Implement this test for missing encryptedSavedObjects + }); + it('should throw if request is missing', async () => { + // TODO: Implement this test for missing request + }); + }); + + describe('getClient', () => { + it('should return clusterClient if key is found', async () => { + // TODO: Implement this test for returning clusterClient when API key is found + }); + + it('should return undefined if no API key is found', async () => { + // TODO: Implement this test for returning undefined when no API key is found + }); + }); + + describe('getRequestFromApiKey', () => { + it('should return a fake request from API key', async () => { + // TODO: Implement this test for returning a fake request from API key + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts index 9d0a4b50141f9..20f32dcd7665a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/constants.ts @@ -6,11 +6,12 @@ */ import { EXCLUDE_ELASTIC_CLOUD_INDICES, INCLUDE_INDEX_PATTERN } from '../../../../common/constants'; + export const SCOPE = ['securitySolution']; export const TYPE = 'entity_analytics:monitoring:privileges:engine'; export const VERSION = '1.0.0'; export const TIMEOUT = '10m'; -export const INTERVAL = '10m'; +export const INTERVAL = this.opts.config.privileges.developer.syncInterval; // 10 minutes in seconds (default) export const PRIVILEGE_MONITORING_ENGINE_STATUS = { // TODO Make the engine initialization async before uncommenting these lines 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 df924f3082fce..6712fb9f417d3 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 @@ -17,6 +17,7 @@ import { EngineComponentResourceEnum } from '../../../../common/api/entity_analy import { startPrivilegeMonitoringTask as mockStartPrivilegeMonitoringTask } from './tasks/privilege_monitoring_task'; import type { AuditLogger } from '@kbn/core/server'; +import { createEsSearchResponse, createMockUsers, withMockLog } from './test_helpers'; import { eventIngestPipeline, PRIVMON_EVENT_INGEST_PIPELINE_ID, @@ -202,13 +203,16 @@ describe('Privilege Monitoring Data Client', () => { }); }); - describe('syncAllIndexUsers', () => { - const mockLog = jest.fn(); - - it('should sync all index users successfully', async () => { + // Below are the tests for the plainIndexSync function and its related functions. + describe('plainIndexSync', () => { + let mockLog: jest.Mock; + beforeEach(() => { + mockLog = withMockLog(dataClient); + }); + it('should sync all usernames from index sources and bulk delete any stale users', async () => { const mockMonitoringSOSources = [ - { name: 'source1', indexPattern: 'index1' }, - { name: 'source2', indexPattern: 'index2' }, + { type: 'index', name: 'source1', indexPattern: 'index1' }, + { type: 'index', name: 'source2', indexPattern: 'index2' }, ]; const findByIndexMock = jest.fn().mockResolvedValue(mockMonitoringSOSources); Object.defineProperty(dataClient, 'monitoringIndexSourceClient', { @@ -218,20 +222,25 @@ describe('Privilege Monitoring Data Client', () => { findByIndex: findByIndexMock, }, }); - dataClient.syncUsernamesFromIndex = jest.fn().mockResolvedValue(['user1', 'user2']); + dataClient.ingestUsersFromIndexSource = jest.fn().mockResolvedValue(['source1', 'source2']); + dataClient.findStaleUsersForIndex = jest.fn().mockResolvedValue(['source1']); + dataClient.bulkDeleteStaleUsers = jest.fn().mockResolvedValue('source1'); await dataClient.plainIndexSync(); expect(findByIndexMock).toHaveBeenCalled(); - expect(dataClient.syncUsernamesFromIndex).toHaveBeenCalledTimes(2); - expect(dataClient.syncUsernamesFromIndex).toHaveBeenCalledWith({ - indexName: 'index1', - kuery: undefined, - }); + expect(mockLog).not.toHaveBeenCalledWith( + 'debug', + 'No monitoring index sources found. Skipping sync.' + ); + expect(dataClient.ingestUsersFromIndexSource).toHaveBeenCalledTimes(2); + expect(dataClient.findStaleUsersForIndex).toHaveBeenCalledTimes(2); + expect(dataClient.bulkDeleteStaleUsers).toHaveBeenCalledTimes(1); }); - it('logs and returns if no index sources', async () => { + it('should log and returns if no index sources', async () => { Object.defineProperty(dataClient, 'log', { value: mockLog }); const findByIndexMock = jest.fn().mockResolvedValue([]); Object.defineProperty(dataClient, 'monitoringIndexSourceClient', { + // TODO: de-duplicate this across tests value: { init: jest.fn().mockResolvedValue({ status: 'success' }), update: jest.fn(), @@ -247,78 +256,399 @@ describe('Privilege Monitoring Data Client', () => { ); }); - it('skips sources without indexPattern', async () => { + it('should skip sources without indexPattern', async () => { Object.defineProperty(dataClient, 'monitoringIndexSourceClient', { value: { - findByIndex: jest.fn().mockResolvedValue([ - { name: 'no-index', indexPattern: undefined }, - { name: 'with-index', indexPattern: 'foo' }, - ]), + findByIndex: jest.fn().mockResolvedValue([{ name: 'no-index', type: 'index' }]), init: jest.fn().mockResolvedValue({ status: 'success' }), update: jest.fn(), }, }); - - dataClient.syncUsernamesFromIndex = jest.fn().mockResolvedValue(['user1']); - Object.defineProperty(dataClient, 'findStaleUsersForIndex', { - value: jest.fn().mockResolvedValue([]), - }); + Object.defineProperty(dataClient, 'log', { value: mockLog }); + dataClient.ingestUsersFromIndexSource = jest.fn().mockResolvedValue(['no-index']); 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, - }); + expect(mockLog).toHaveBeenCalledWith( + 'debug', + 'Skipping source "no-index" with no index pattern.' + ); }); + }); - it('should retrieve all usernames from index and perform bulk ops', async () => { - const mockHits = [ + describe('ingestUsersFromIndexSource', () => { + let mockLog: jest.Mock; + beforeEach(() => { + mockLog = withMockLog(dataClient); + }); + it('should return usernames from the index source', async () => { + dataClient.fetchUsernamesFromIndex = jest.fn().mockResolvedValue(['username1', 'username2']); + dataClient.bulkUpsertMonitoredUsers = jest.fn().mockResolvedValue(['username1', 'username2']); + dataClient.createOrUpdateIndex = jest.fn().mockResolvedValue(['username1', 'username2']); + const index: string = 'index1'; + const mockSource = { type: 'index', name: 'username1', indexPattern: index }; + const result = await dataClient.ingestUsersFromIndexSource(mockSource, index); + expect(result).toEqual(['username1', 'username2']); + }); + + it('should handle index not found error gracefully', async () => { + const indexNotFoundMockError = { + meta: { + body: { + error: { + type: 'index_not_found_exception', + }, + }, + }, + }; + dataClient.fetchUsernamesFromIndex = jest.fn().mockRejectedValue(indexNotFoundMockError); + await dataClient.ingestUsersFromIndexSource( + { type: 'index', name: 'source1', indexPattern: 'index1' }, + 'index1' + ); + expect(mockLog).toHaveBeenCalledWith('warn', `Index "index1" not found — skipping.`); + }); + it('should handle errors during user ingestion', async () => { + const genericErrorMock = new Error('generic_error'); + + dataClient.fetchUsernamesFromIndex = jest.fn().mockRejectedValue(genericErrorMock); + await dataClient.ingestUsersFromIndexSource( + { type: 'index', name: 'source1', indexPattern: 'index1' }, + 'index1' + ); + expect(mockLog).toHaveBeenCalledWith( + 'error', + 'Unexpected error during sync for index "index1": generic_error' + ); + }); + }); + + describe('findStaleUsersForIndex', () => { + it('should return stale users for a given index', async () => { + const esClientResponse = createEsSearchResponse([ { - _source: { user: { name: 'frodo' } }, - _id: '1', - sort: [1], + _id: 'abc123', + _index: 'index1', + _source: { + user: { name: 'alice' }, + labels: { source_indices: ['index1'] }, + }, }, + ]); + esClientMock.search.mockResolvedValue(esClientResponse); + const expectedBulkUsernames = [ { - _source: { user: { name: 'samwise' } }, - _id: '2', - sort: [2], + username: 'alice', + existingUserId: 'abc123', + indexName: 'index1', }, ]; + const result = await dataClient.findStaleUsersForIndex('index1', ['bob']); + expect(result).toEqual(expectedBulkUsernames); + }); + it('should use "unknown" as username if user.name is missing', async () => { + const esClientResponse = createEsSearchResponse([ + { + _id: 'abc123', + _index: 'index1', + _source: { + user: {}, + labels: { source_indices: ['index1'] }, + }, + }, + ]); + + esClientMock.search.mockResolvedValue(esClientResponse); + const result = await dataClient.findStaleUsersForIndex('index1', ['bob']); - const mockMonitoredUserHits = { + expect(result).toEqual([ + { + username: 'unknown', + existingUserId: 'abc123', + indexName: 'index1', + }, + ]); + }); + }); + describe('bulkUpsertMonitoredUsers', () => { + let mockLog: jest.Mock; + beforeEach(() => { + mockLog = withMockLog(dataClient); + }); + it('should bulk upsert monitored users', async () => { + const mockUsernames = ['alice', 'bob']; + dataClient.getMonitoredUsers = jest.fn().mockResolvedValue({ hits: { hits: [ { - _source: { user: { name: 'frodo' } }, - _id: '1', + _id: 'id-alice', + _source: { user: { name: 'alice' } }, }, { - _source: { user: { name: 'samwise' } }, - _id: '2', + _id: 'id-bob', + _source: { user: { name: 'bob' } }, }, ], }, - }; + }); + const result = await dataClient.bulkUpsertMonitoredUsers({ + usernames: mockUsernames, + indexName: 'privilege_monitoring_users', + }); + expect(result).toEqual(mockUsernames); + expect(esClientMock.bulk).toHaveBeenCalledWith({ + refresh: 'wait_for', + body: expect.arrayContaining([ + { + update: { + _index: '.entity_analytics.monitoring.users-default', + _id: 'id-alice', + }, + }, + { + script: { + source: expect.stringContaining('if (!ctx._source.labels.source_indices.contains'), + params: { + index: 'privilege_monitoring_users', + }, + }, + }, + { + update: { + _index: '.entity_analytics.monitoring.users-default', + _id: 'id-bob', + }, + }, + { + script: { + source: expect.stringContaining('if (!ctx._source.labels.source_indices.contains'), + params: { + index: 'privilege_monitoring_users', + }, + }, + }, + ]), + }); + }); + + it('should handle errors during bulk upsert', async () => { + const mockError = new Error('Bulk upsert failed'); + dataClient.getMonitoredUsers = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { + _id: 'id-alice', + _source: { user: { name: 'alice' } }, + }, + ], + }, + }); + esClientMock.bulk.mockRejectedValue(mockError); + + const result = await dataClient.bulkUpsertMonitoredUsers({ + usernames: ['alice'], + indexName: 'privilege_monitoring_users', + }); - dataClient.searchUsernamesInIndex = jest + expect(result).toEqual(['alice']); + + expect(mockLog).toHaveBeenCalledWith('error', expect.stringContaining('Bulk upsert failed')); + }); + + it('should handle empty usernames gracefully', async () => { + const result = await dataClient.bulkUpsertMonitoredUsers({ + usernames: [], + indexName: 'privilege_monitoring_users', + }); + expect(result).toEqual(null); + expect(esClientMock.bulk).not.toHaveBeenCalled(); + }); + }); + describe('buildBulkOperationsForUsers', () => { + it('should build bulk operations for updating users', () => { + const mockUsers = [ + { username: 'alice', existingUserId: 'alice', indexName: 'privilege_monitoring_users' }, + { username: 'bob', existingUserId: 'bob', indexName: 'privilege_monitoring_users' }, + ]; + const result = dataClient.buildBulkOperationsForUsers( + mockUsers, + 'privilege_monitoring_users' + ); + expect(result).toEqual([ + { + update: { + _index: 'privilege_monitoring_users', + _id: 'alice', + }, + }, + { + script: { + source: expect.stringContaining('if (!ctx._source.labels.source_indices.contains'), + params: { + index: 'privilege_monitoring_users', + }, + }, + }, + { + update: { + _index: 'privilege_monitoring_users', + _id: 'bob', + }, + }, + { + script: { + source: expect.stringContaining('if (!ctx._source.labels.source_indices.contains'), + params: { + index: 'privilege_monitoring_users', + }, + }, + }, + ]); + }); + it('should build bulk operations for new users', () => { + const mockUsers = [ + { username: 'charlie', existingUserId: undefined, indexName: 'privilege_monitoring_users' }, + ]; + const result = dataClient.buildBulkOperationsForUsers( + mockUsers, + 'privilege_monitoring_users' + ); + expect(result).toEqual([ + { + index: { + _index: 'privilege_monitoring_users', + }, + }, + { + user: { is_privileged: true, name: 'charlie' }, + labels: { + source_indices: ['privilege_monitoring_users'], + sources: ['index'], + }, + }, + ]); + }); + it('should handle a mix of existing and new users', () => { + const mockUsers = [ + { username: 'alice', existingUserId: 'id-alice', indexName: 'privilege_monitoring_users' }, + { username: 'bob', existingUserId: undefined, indexName: 'privilege_monitoring_users' }, + ]; + + const result = dataClient.buildBulkOperationsForUsers( + mockUsers, + 'privilege_monitoring_users' + ); + + expect(result).toEqual([ + { + update: { + _index: 'privilege_monitoring_users', + _id: 'id-alice', + }, + }, + { + script: { + source: expect.stringContaining('ctx._source.labels.source_indices'), + params: { index: 'privilege_monitoring_users' }, + }, + }, + { + index: { + _index: 'privilege_monitoring_users', + }, + }, + { + user: { name: 'bob', is_privileged: true }, + labels: { + sources: ['index'], + source_indices: ['privilege_monitoring_users'], + }, + }, + ]); + }); + it('should return an empty array when given no users', () => { + const result = dataClient.buildBulkOperationsForUsers([], 'privilege_monitoring_users'); + expect(result).toEqual([]); + }); + }); + describe('bulkDeleteStaleUsers', () => { + let mockLog: jest.Mock; + beforeEach(() => { + mockLog = withMockLog(dataClient); + }); + it('should bulk delete stale users', async () => { + const mockStaleUsers = createMockUsers([ + { username: 'alice', id: 'id-alice' }, + { username: 'bob', id: 'id-bob' }, + ]); + + const bulkOperationsForSoftDeleteUsersResult = [ + { + update: { + _index: '.entity_analytics.monitoring.users-default', + _id: 'id-alice', + }, + }, + { + script: { + source: expect.stringContaining('ctx._source.labels?.source_indices.removeIf'), + params: { + index: 'privilege_monitoring_users', + }, + }, + }, + { + update: { + _index: '.entity_analytics.monitoring.users-default', + _id: 'id-bob', + }, + }, + { + script: { + source: expect.stringContaining('ctx._source.labels?.source_indices.removeIf'), + params: { + index: 'privilege_monitoring_users', + }, + }, + }, + ]; + + dataClient.bulkOperationsForSoftDeleteUsers = jest .fn() - .mockResolvedValueOnce({ hits: { hits: mockHits } }) // first batch - .mockResolvedValueOnce({ hits: { hits: [] } }); // second batch = end + .mockReturnValue(bulkOperationsForSoftDeleteUsersResult); - dataClient.getMonitoredUsers = jest.fn().mockResolvedValue(mockMonitoredUserHits); - dataClient.buildBulkOperationsForUsers = jest.fn().mockReturnValue([{ index: { _id: '1' } }]); - dataClient.getIndex = jest.fn().mockReturnValue('test-index'); + await dataClient.bulkDeleteStaleUsers(mockStaleUsers); - const usernames = await dataClient.syncUsernamesFromIndex({ - indexName: 'test-index', + expect(esClientMock.bulk).toHaveBeenCalledWith({ + refresh: 'wait_for', + body: bulkOperationsForSoftDeleteUsersResult, }); + }); + it('should handle errors during bulk delete', async () => { + const mockStaleUsers = createMockUsers([ + { username: 'alice', id: 'id-alice' }, + { username: 'bob', id: 'id-bob' }, + ]); + const mockError = new Error('Bulk delete failed'); + dataClient.bulkOperationsForSoftDeleteUsers = jest.fn().mockReturnValue([ + { + update: { + _index: '.entity_analytics.monitoring.users-default', + _id: 'id-alice', + }, + }, + { + script: { + source: expect.stringContaining('ctx._source.labels?.source_indices.removeIf'), + params: { + index: 'privilege_monitoring_users', + }, + }, + }, + ]); + esClientMock.bulk.mockRejectedValue(mockError); + + await dataClient.bulkDeleteStaleUsers(mockStaleUsers); - expect(usernames).toEqual(['frodo', 'samwise']); - expect(dataClient.searchUsernamesInIndex).toHaveBeenCalledTimes(2); - expect(esClientMock.bulk).toHaveBeenCalled(); - expect(dataClient.getMonitoredUsers).toHaveBeenCalledWith(['frodo', 'samwise']); - expect(dataClient.buildBulkOperationsForUsers).toHaveBeenCalled(); + expect(mockLog).toHaveBeenCalledWith('error', expect.stringContaining('Bulk delete failed')); }); }); }); 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 c16b5761c7b02..07d022a17392b 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 @@ -66,7 +66,8 @@ 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 { BulkOperation, PrivMonBulkUser, PrivMonUserSource } from './types'; +import type { MonitoringEntitySourceDescriptor } from './saved_objects'; import { PrivilegeMonitoringEngineDescriptorClient, MonitoringEntitySourceDescriptorClient, @@ -75,6 +76,8 @@ import { PRIVMON_EVENT_INGEST_PIPELINE_ID, eventIngestPipeline, } from './elasticsearch/pipelines/event_ingested'; +import type { MonitoringSyncIntervalConfig } from '../types'; + interface PrivilegeMonitoringClientOpts { logger: Logger; clusterClient: IScopedClusterClient; @@ -85,6 +88,7 @@ interface PrivilegeMonitoringClientOpts { kibanaVersion: string; telemetry?: AnalyticsServiceSetup; apiKeyManager?: ApiKeyManager; + config: MonitoringSyncIntervalConfig; } export class PrivilegeMonitoringDataClient { @@ -149,6 +153,10 @@ export class PrivilegeMonitoringDataClient { await this.apiKeyGenerator.generate(); } + this.log( + 'info', + 'Starting privilege monitoring task with interval ${JSON.stringify(this.configInterval)}' + ); await startPrivilegeMonitoringTask({ logger: this.opts.logger, namespace: this.opts.namespace, @@ -444,64 +452,74 @@ export class PrivilegeMonitoringDataClient { */ public async plainIndexSync() { // get all monitoring index source saved objects of type 'index' - const indexSources = await this.monitoringIndexSourceClient.findByIndex(); - if (indexSources.length === 0) { + const monitoringIndexSources: MonitoringEntitySourceDescriptor[] = + await this.monitoringIndexSourceClient.findByIndex(); + const allStaleUsers: PrivMonBulkUser[] = []; + + if (monitoringIndexSources.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; + for (const source of monitoringIndexSources) { + if (!source.indexPattern) { + this.log('debug', `Skipping source "${source.name}" with no index pattern.`); + } else { + const index: string = source.indexPattern; + const syncedUsernames = await this.ingestUsersFromIndexSource(source, index); + if (syncedUsernames) { + allStaleUsers.push(...(await this.findStaleUsersForIndex(index, syncedUsernames))); } - 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 }); + if (allStaleUsers.length > 0) await this.bulkDeleteStaleUsers(allStaleUsers); + } + + public async ingestUsersFromIndexSource( + source: MonitoringEntitySourceDescriptor, + index: string + ): Promise { + try { + const usernames = await this.fetchUsernamesFromIndex({ + indexName: index, + kuery: source.filter?.kuery, + }); + + const result = await this.bulkUpsertMonitoredUsers({ + usernames, + indexName: index, + }); + + if (!result) { + this.log('debug', `No monitored users ingested for index "${index}"`); + return null; + } + + return result; + } catch (error) { + const isIndexNotFound = + error?.meta?.body?.error?.type === 'index_not_found_exception' || + error?.message?.includes('index_not_found_exception'); + + const level = isIndexNotFound ? 'warn' : 'error'; + const msg = isIndexNotFound + ? `Index "${index}" not found — skipping.` + : `Unexpected error during sync for index "${index}": ${error.message}`; + + this.log(level, msg); + return null; } } /** - * Synchronizes usernames from a specified index by collecting them in batches - * and performing create or update operations in the privileged user index. + * Fetches usernames from a specified index using a query. + * It retrieves usernames in batches, handling pagination with `search_after`. * - * 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. + * @param indexName - The name of the index to search. + * @param kuery - Optional KQL query to filter results. + * @returns A promise that resolves to an array of usernames. */ - public async syncUsernamesFromIndex({ + public async fetchUsernamesFromIndex({ indexName, kuery, }: { @@ -513,6 +531,7 @@ export class PrivilegeMonitoringDataClient { const batchSize = 100; const query = kuery ? toElasticsearchQuery(fromKueryExpression(kuery)) : { match_all: {} }; + while (true) { const response = await this.searchUsernamesInIndex({ indexName, @@ -524,49 +543,79 @@ export class PrivilegeMonitoringDataClient { 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), - })); - - if (usersToWrite.length === 0) return batchUsernames; - - 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( + public async bulkDeleteStaleUsers(staleUsernames: PrivMonBulkUser[]) { + try { + this.log('debug', `Found ${staleUsernames.length} stale users to soft delete`); + const ops = this.bulkOperationsForSoftDeleteUsers(staleUsernames, this.getIndex()); + await this.esClient.bulk({ refresh: 'wait_for', body: ops }); + } catch (error) { + this.log('error', `Error executing bulk soft delete operations: ${error}`); + } + } + + /** + * Synchronizes a list of usernames with the monitoring index. + * It checks for existing users, updates them if they exist, or creates new ones if they don't. + * + * @param usernames - An array of usernames to sync. + * @param indexName - The name of the index to associate with the users. + * @returns A promise that resolves to an array of usernames that were synced. + */ + public async bulkUpsertMonitoredUsers({ + usernames, + indexName, + }: { + usernames: string[]; + indexName: string; + }): Promise { + if (usernames.length === 0) { + this.log('debug', 'No usernames to sync'); + return null; + } + + const existingUserRes = await this.getMonitoredUsers(usernames); + + 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[] = usernames.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({ refresh: 'wait_for', body: ops }); + } catch (error) { + this.log('error', `Error executing bulk operations: ${error}`); + } + return usernames; + } + public async findStaleUsersForIndex( indexName: string, userNames: string[] ): Promise { const response = await this.esClient.search({ index: this.getIndex(), - size: 10, // check this + size: 10, _source: ['user.name', 'labels.source_indices'], query: { bool: { @@ -614,8 +663,11 @@ export class PrivilegeMonitoringDataClient { * @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[] = []; + public buildBulkOperationsForUsers( + users: PrivMonBulkUser[], + userIndexName: string + ): BulkOperation[] { + const ops: BulkOperation[] = []; this.log('info', `Building bulk operations for ${users.length} users`); for (const user of users) { if (user.existingUserId) { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/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 index 59c38f8b54d09..ea5a6cac88117 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/routes/monitoring_entity_source/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 @@ -22,6 +22,7 @@ import { type CreateEntitySourceResponse, GetEntitySourceRequestParams, UpdateEntitySourceRequestParams, + DeleteEntitySourceRequestParams, } from '../../../../../../common/api/entity_analytics/privilege_monitoring/monitoring_entity_source/monitoring_entity_source.gen'; export const monitoringEntitySourceRoute = ( @@ -68,6 +69,42 @@ export const monitoringEntitySourceRoute = ( } ); + router.versioned + .delete({ + access: 'public', + path: '/api/entity_analytics/monitoring/entity_source/{id}', + security: { + authz: { + requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`], + }, + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: { + params: DeleteEntitySourceRequestParams, + }, + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const secSol = await context.securitySolution; + await secSol.getMonitoringEntitySourceDataClient().delete(request.params.id); + return response.ok({ body: { acknowledged: true } }); + } catch (e) { + const error = transformError(e); + logger.error(`Error deleting user: ${error.message}`); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); router.versioned .get({ access: 'public', 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 index b419d5d7a0947..c95493cef2d9e 100644 --- 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 @@ -22,7 +22,6 @@ export class MonitoringEntitySourceDescriptorClient { async create(attributes: CreateMonitoringEntitySource) { await this.assertNameUniqueness(attributes); - const { id, attributes: created } = await this.dependencies.soClient.create( monitoringEntitySourceTypeName, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts index ec8be11771cdc..9a48e6f00a777 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/tasks/privilege_monitoring_task.ts @@ -191,9 +191,10 @@ export const startPrivilegeMonitoringTask = async ({ logger, namespace, taskManager, -}: StartParams) => { + interval = INTERVAL, +}: StartParams & { interval?: SyncIntervalConfig }) => { const taskId = getTaskId(namespace); - + logger.info(`Starting privilege monitoring task with id ${taskId} and interval ${interval}`); try { await taskManager.ensureScheduled({ id: taskId, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/test_helpers.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/test_helpers.ts new file mode 100644 index 0000000000000..9977f80ba3c55 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/privilege_monitoring/test_helpers.ts @@ -0,0 +1,49 @@ +/* + * 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 { PrivMonBulkUser } from './types'; + +export const createMockUsers = ( + entries: Array<{ username: string; id?: string }>, + indexName = 'privilege_monitoring_users' +): PrivMonBulkUser[] => { + return entries.map(({ username, id }) => ({ + username, + existingUserId: id, + indexName, + })); +}; + +export function createEsSearchResponse( + hits: Array<{ _id: string; _index: string; _source: T }> +) { + return { + took: 1, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: hits.length, + max_score: 1.0, + hits: hits.map((hit) => ({ + _id: hit._id, + _index: hit._index, + _source: hit._source, + _score: 1.0, + })), + }, + }; +} + +export const withMockLog = (client: unknown, mock = jest.fn()) => { + Object.defineProperty(client as unknown, 'log', { value: mock }); + return mock; +}; 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 5271edce0a1ec..36d94cf05484f 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,9 +6,54 @@ */ export type PrivMonUserSource = 'csv' | 'api' | 'index_sync'; +export type SyncIntervalConfig = EntityAnalyticsConfig['monitoring']['privileges']['syncInterval']; export interface PrivMonBulkUser { username: string; indexName: string; existingUserId?: string; } + +// For update metadata +export interface BulkUpdateOperation { + update: { + _index: string; + _id: string; + }; +} + +// For the Painless script used in updates +export interface BulkScriptOperation { + script: { + source: string; + params: { + index: string; + }; + }; +} + +// For create (index) metadata +export interface BulkIndexOperation { + index: { + _index: string; + }; +} + +// For the actual document to insert (new user) +export interface BulkDocumentBody { + user: { + name: string; + is_privileged: boolean; + }; + labels: { + sources: string[]; + source_indices: string[]; + }; +} + +// Union of all operation parts +export type BulkOperation = + | BulkUpdateOperation + | BulkScriptOperation + | BulkIndexOperation + | BulkDocumentBody; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/types.ts index 1ea2f3d5a3864..94eb43acdca83 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/types.ts @@ -18,7 +18,7 @@ import type { ConfigType } from '../../config'; import type { StartPlugins } from '../../plugin'; import type { SecuritySolutionPluginRouter } from '../../types'; export type EntityAnalyticsConfig = ConfigType['entityAnalytics']; - +export type MonitoringSyncIntervalConfig = ConfigType['monitoring']; export interface EntityAnalyticsRoutesDeps { router: SecuritySolutionPluginRouter; logger: Logger; diff --git a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts index 428236f6b877a..162c58d0719f1 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/request_context_factory.ts @@ -289,6 +289,7 @@ export class RequestContextFactory implements IRequestContextFactory { auditLogger: getAuditLogger(), kibanaVersion: options.kibanaVersion, telemetry: core.analytics, + config: config.entityAnalytics.monitoring, }); }), getMonitoringEntitySourceDataClient: memoize(() => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/engine.ts index 101fda6bf3a07..fbb1c73d20752 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/engine.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/engine.ts @@ -6,13 +6,19 @@ */ import expect from '@kbn/expect'; +import { privilegeMonitoringTypeName } from '@kbn/security-solution-plugin/server/lib/entity_analytics/privilege_monitoring/saved_objects/privilege_monitoring_type'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { dataViewRouteHelpersFactory } from '../../utils/data_view'; +import { privilegeMonitoringRouteHelpersFactory } from '../../utils/privilege_monitoring'; export default ({ getService }: FtrProviderContext) => { const api = getService('securitySolutionApi'); const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const privmon = privilegeMonitoringRouteHelpersFactory(supertest); const log = getService('log'); + const es = getService('es'); + const spaceName = 'default'; describe('@ess @serverless @skipInServerlessMKI Entity Privilege Monitoring APIs', () => { const dataView = dataViewRouteHelpersFactory(supertest); @@ -37,5 +43,114 @@ export default ({ getService }: FtrProviderContext) => { expect(res.status).eql(200); }); }); + + describe('plain index sync', () => { + log.info(`Syncing plain index`); + // Want to make sure that monitoring saved objects are created and have something in them, + const indexName = 'tatooine-privileged-users'; + const entitySource = { + type: 'index' as const, + name: 'StarWars', + managed: true, + indexPattern: indexName, + enabled: true, + matchers: [ + { + fields: ['user.role'], + values: ['admin'], + }, + ], + filter: {}, + }; + after(async () => { + // Probable do not need cleanup before AND after. Debugging WIP + await es.indices.delete({ index: indexName }, { ignore: [404] }); + await es.indices.delete({ index: 'default-monitoring-index' }, { ignore: [404] }); + await privmon.deleteIndexSource('default-monitoring-index', { ignore404: true }); + await privmon.deleteIndexSource(entitySource.name, { ignore404: true }); + }); + before(async () => { + const soId = await kibanaServer.savedObjects.find<{ + type: typeof privilegeMonitoringTypeName; + space: string; + }>({ + type: privilegeMonitoringTypeName, + space: spaceName, + }); + if (soId.saved_objects.length !== 0) { + await kibanaServer.savedObjects.delete({ + type: privilegeMonitoringTypeName, + space: spaceName, + id: soId.saved_objects[0].id, + }); + } + // Delete quickly any existing source with the same name + // Ensure index does not exist before creating it + await es.indices.delete({ index: indexName }, { ignore: [404] }); + await es.indices.delete({ index: 'default-monitoring-index' }, { ignore: [404] }); + await privmon.deleteIndexSource('default-monitoring-index', { ignore404: true }); + await privmon.deleteIndexSource(entitySource.name, { ignore404: true }); + // await privmon.deleteIndexSource(entitySource.name); + // Create index with mapping + await es.indices.create({ + index: indexName, + mappings: { + properties: { + user: { + properties: { + name: { + type: 'keyword', + fields: { + text: { type: 'text' }, + }, + }, + role: { + type: 'keyword', + }, + }, + }, + }, + }, + }); + // Bulk insert documents + const bulkBody = [ + 'Luke Skywalker', + 'Leia Organa', + 'Han Solo', + 'Chewbacca', + 'Obi-Wan Kenobi', + 'Yoda', + 'R2-D2', + 'C-3PO', + 'Darth Vader', + ].flatMap((name) => [{ index: {} }, { user: { name } }]); + await es.bulk({ index: indexName, body: bulkBody, refresh: true }); + }); + + it('should sync plain index', async () => { + // Register monitoring source + const response = await privmon.registerIndexSource(entitySource); + expect(response.status).to.be(200); + // Call init to trigger the sync + await privmon.initEngine(); + // default-monitoring-index should exist now + const result = await kibanaServer.savedObjects.find({ + type: 'entity-analytics-monitoring-entity-source', + space: 'default', + }); + + const names = result.saved_objects.map((so) => so.attributes.name); + expect(names).to.contain('default-monitoring-index'); + // How to make interval shorter to test the data syncs? + // Now list the users in monitoring + // const res = await privmon.listUsers(); // this should be undefined to start with. Can we change the interval to 1s? + // console.log('Users in monitoring:', res.body.users); + // const userNames = res.body.users.map((u: any) => u.name); + // expect(userNames).to.be.an('array'); + // expect(userNames.length).to.be.greaterThan(0); + // expect(userNames).contain('Luke Skywalker'); + // expect(userNames).contain('Leia Organa'); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/privilege_monitoring_privileges_check.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/privilege_monitoring_privileges_check.ts index 34ba28cfb100e..0a59c474bad5a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/privilege_monitoring_privileges_check.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/monitoring/trial_license_complete_tier/privilege_monitoring_privileges_check.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { SECURITY_FEATURE_ID } from '@kbn/security-solution-plugin/common'; import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { privilegeMonitoringRouteHelpersFactoryNoAuth } from '../../utils/privilege_monitoring'; +import { privilegeMonitoringRouteHelpersFactory } from '../../utils/privilege_monitoring'; import { usersAndRolesFactory } from '../../utils/users_and_roles'; const USER_PASSWORD = 'changeme'; @@ -64,7 +64,7 @@ const ROLES = [READ_ALL_INDICES_ROLE, READ_PRIV_MON_INDICES_ROLE, READ_NO_INDEX_ export default ({ getService }: FtrProviderContext) => { const supertestWithoutAuth = getService('supertestWithoutAuth'); - const privMonRoutesNoAuth = privilegeMonitoringRouteHelpersFactoryNoAuth(supertestWithoutAuth); + const privMonRoutesNoAuth = privilegeMonitoringRouteHelpersFactory(supertestWithoutAuth); const userHelper = usersAndRolesFactory(getService('security')); async function createPrivilegeTestUsers() { diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/privilege_monitoring.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/privilege_monitoring.ts index c877ba40bee1d..e6d54a05d0d55 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/privilege_monitoring.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/privilege_monitoring.ts @@ -4,19 +4,73 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +/* eslint-disable no-console */ + import { X_ELASTIC_INTERNAL_ORIGIN_REQUEST } from '@kbn/core-http-common'; import { SupertestWithoutAuthProviderType } from '@kbn/ftr-common-functional-services'; import { API_VERSIONS } from '@kbn/security-solution-plugin/common/constants'; -export const privilegeMonitoringRouteHelpersFactoryNoAuth = ( - supertestWithoutAuth: SupertestWithoutAuthProviderType -) => ({ - privilegesForUser: async ({ username, password }: { username: string; password: string }) => - await supertestWithoutAuth - .get('/api/entity_analytics/monitoring/privileges/privileges') - .auth(username, password) +export const privilegeMonitoringRouteHelpersFactory = ( + supertest: SupertestWithoutAuthProviderType +) => { + const setHeaders = (req: any) => + req .set('elastic-api-version', API_VERSIONS.public.v1) .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') - .send() - .expect(200), -}); + .set('kbn-xsrf', 'true'); + + return { + privilegesForUser: async ({ username, password }: { username: string; password: string }) => { + return await setHeaders( + supertest + .get('/api/entity_analytics/monitoring/privileges/privileges') + .auth(username, password) + ) + .send() + .expect(200); + }, + + registerIndexSource: async (source: any) => { + const response = await setHeaders( + supertest.post('/api/entity_analytics/monitoring/entity_source') + ).send(source); + + if (response.status !== 200) { + console.error('registerIndexSource failed with status:', response.status); + console.error('Response body:', JSON.stringify(response.body, null, 2)); + console.error('Sent payload:', JSON.stringify(source, null, 2)); + } + + // expect(response.status).to.be.equal(200); + return response; + }, + + initEngine: async () => { + return await setHeaders(supertest.post('/api/entity_analytics/monitoring/engine/init')) + .send() + .expect(200); + }, + + listUsers: async () => { + return await setHeaders(supertest.get('/api/entity_analytics/monitoring/users/list')).expect( + 200 + ); + }, + + deleteIndexSource: async (sourceId: string, { ignore404 = false } = {}) => { + const res = await setHeaders( + supertest.delete(`/api/entity_analytics/monitoring/entity_source/${sourceId}`) + ).catch((err: { status: number }) => { + if (ignore404 && err.status === 404) return { status: 404 }; + throw err; + }); + + if (!ignore404 && res.status !== 200) { + throw new Error(`Expected 200 OK, got ${res.status}`); + } + + return res; + }, + }; +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts index 159c37f44d64e..02030f00cb12c 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts @@ -411,7 +411,7 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/get", "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", - "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privilege-monitoring-status/close_point_in_time", "saved_object:privilege-monitoring-status/create", "saved_object:privilege-monitoring-status/bulk_create", "saved_object:privilege-monitoring-status/update", @@ -1422,7 +1422,7 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/get", "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", - "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privilege-monitoring-status/close_point_in_time", "saved_object:privilege-monitoring-status/create", "saved_object:privilege-monitoring-status/bulk_create", "saved_object:privilege-monitoring-status/update", @@ -2186,6 +2186,13 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privmon-api-key/find", "saved_object:privmon-api-key/open_point_in_time", "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -2622,6 +2629,13 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privmon-api-key/find", "saved_object:privmon-api-key/open_point_in_time", "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -3157,7 +3171,7 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/get", "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", - "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privilege-monitoring-status/close_point_in_time", "saved_object:privilege-monitoring-status/create", "saved_object:privilege-monitoring-status/bulk_create", "saved_object:privilege-monitoring-status/update", @@ -4107,6 +4121,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:privilege-monitoring-status/create", "saved_object:privilege-monitoring-status/bulk_create", "saved_object:privilege-monitoring-status/update", @@ -4810,6 +4836,13 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privmon-api-key/find", "saved_object:privmon-api-key/open_point_in_time", "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -5218,6 +5251,13 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privmon-api-key/find", "saved_object:privmon-api-key/open_point_in_time", "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -5739,7 +5779,19 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/get", "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", - "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privmon-api-key/bulk_get", + "saved_object:privmon-api-key/get", + "saved_object:privmon-api-key/find", + "saved_object:privmon-api-key/open_point_in_time", + "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", + "saved_object:privilege-monitoring-status/close_point_in_time", "saved_object:privilege-monitoring-status/create", "saved_object:privilege-monitoring-status/bulk_create", "saved_object:privilege-monitoring-status/update", @@ -6657,7 +6709,7 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privilege-monitoring-status/get", "saved_object:privilege-monitoring-status/find", "saved_object:privilege-monitoring-status/open_point_in_time", - "saved_object:privilege-monitoring-status/close_point_in_time", + "saved_object:privilege-monitoring-status/close_point_in_time", "saved_object:privilege-monitoring-status/create", "saved_object:privilege-monitoring-status/bulk_create", "saved_object:privilege-monitoring-status/update", @@ -7354,6 +7406,13 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privmon-api-key/find", "saved_object:privmon-api-key/open_point_in_time", "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find", @@ -7752,6 +7811,13 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:privmon-api-key/find", "saved_object:privmon-api-key/open_point_in_time", "saved_object:privmon-api-key/close_point_in_time", + "saved_object:privmon-api-key/create", + "saved_object:privmon-api-key/bulk_create", + "saved_object:privmon-api-key/update", + "saved_object:privmon-api-key/bulk_update", + "saved_object:privmon-api-key/delete", + "saved_object:privmon-api-key/bulk_delete", + "saved_object:privmon-api-key/share_to_space", "saved_object:entity-analytics-monitoring-entity-source/bulk_get", "saved_object:entity-analytics-monitoring-entity-source/get", "saved_object:entity-analytics-monitoring-entity-source/find",