diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts index e99358bca1ac8..e40a18fbe480f 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts @@ -68,7 +68,9 @@ export class EntityStoreDataClient { const definition = getEntityDefinition(entityType, this.options.namespace); - logger.info(`Initializing entity store for ${entityType}`); + logger.info( + `In namespace ${this.options.namespace}: Initializing entity store for ${entityType}` + ); const descriptor = await this.engineClient.init(entityType, definition, filter); await entityClient.createEntityDefinition({ @@ -92,11 +94,13 @@ export class EntityStoreDataClient { if (descriptor.status !== ENGINE_STATUS.STOPPED) { throw new Error( - `Cannot start Entity engine for ${entityType} when current status is: ${descriptor.status}` + `In namespace ${this.options.namespace}: Cannot start Entity engine for ${entityType} when current status is: ${descriptor.status}` ); } - this.options.logger.info(`Starting entity store for ${entityType}`); + this.options.logger.info( + `In namespace ${this.options.namespace}: Starting entity store for ${entityType}` + ); await this.options.entityClient.startEntityDefinition(definition); return this.engineClient.update(definition.id, ENGINE_STATUS.STARTED); @@ -109,11 +113,13 @@ export class EntityStoreDataClient { if (descriptor.status !== ENGINE_STATUS.STARTED) { throw new Error( - `Cannot stop Entity engine for ${entityType} when current status is: ${descriptor.status}` + `In namespace ${this.options.namespace}: Cannot stop Entity engine for ${entityType} when current status is: ${descriptor.status}` ); } - this.options.logger.info(`Stopping entity store for ${entityType}`); + this.options.logger.info( + `In namespace ${this.options.namespace}: Stopping entity store for ${entityType}` + ); await this.options.entityClient.stopEntityDefinition(definition); return this.engineClient.update(definition.id, ENGINE_STATUS.STOPPED); @@ -130,7 +136,9 @@ export class EntityStoreDataClient { public async delete(entityType: EntityType, deleteData: boolean) { const { id } = getEntityDefinition(entityType, this.options.namespace); - this.options.logger.info(`Deleting entity store for ${entityType}`); + this.options.logger.info( + `In namespace ${this.options.namespace}: Deleting entity store for ${entityType}` + ); await this.options.entityClient.deleteEntityDefinition({ id, deleteData }); await this.engineClient.delete(id); diff --git a/x-pack/test/security_solution_api_integration/package.json b/x-pack/test/security_solution_api_integration/package.json index 726d6b61feed5..1649c79f52a7d 100644 --- a/x-pack/test/security_solution_api_integration/package.json +++ b/x-pack/test/security_solution_api_integration/package.json @@ -63,7 +63,7 @@ "nlp_cleanup_task:essentials:qa:serverless:release": "npm run run-tests:genai:basic_essentials nlp_cleanup_task serverless qaEnv", "entity_analytics:server:serverless": "npm run initialize-server:ea:trial_complete risk_engine serverless", - "entity_analytics:runner:serverless": "npm run run-tests:ea:trial_complete risk_engine serverless serverlessEnv", + "entity_analytics:runner:serverless": "npm run run-tests:ea:trial_complete risk_engine serverless serverlessEnv --", "entity_analytics:qa:serverless": "npm run run-tests:ea:trial_complete risk_engine serverless qaPeriodicEnv", "entity_analytics:qa:serverless:release": "npm run run-tests:ea:trial_complete risk_engine serverless qaEnv", "entity_analytics:server:ess": "npm run initialize-server:ea:trial_complete risk_engine ess", @@ -76,6 +76,13 @@ "entity_analytics:essentials:server:ess": "npm run initialize-server:ea:basic_essentials risk_engine ess", "entity_analytics:essentials:runner:ess": "npm run run-tests:ea:basic_essentials risk_engine ess essEnv", + "entity_analytics:entity_store:server:serverless": "npm run initialize-server:ea:trial_complete entity_store serverless", + "entity_analytics:entity_store:runner:serverless": "npm run run-tests:ea:trial_complete entity_store serverless serverlessEnv", + "entity_analytics:entity_store:qa:serverless": "npm run run-tests:ea:trial_complete entity_store serverless qaPeriodicEnv", + "entity_analytics:entity_store:qa:serverless:release": "npm run run-tests:ea:trial_complete entity_store serverless qaEnv", + "entity_analytics:entity_store:server:ess": "npm run initialize-server:ea:trial_complete entity_store ess", + "entity_analytics:entity_store:runner:ess": "npm run run-tests:ea:trial_complete entity_store ess essEnv --", + "edr_workflows:artifacts:server:serverless": "npm run initialize-server:edr-workflows artifacts serverless", "edr_workflows:artifacts:runner:serverless": "npm run run-tests:edr-workflows artifacts serverless serverlessEnv", "edr_workflows:artifacts:qa:serverless": "npm run run-tests:edr-workflows artifacts serverless qaPeriodicEnv", diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts index 8d57ff428e507..99d84fbc5427b 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts @@ -6,104 +6,57 @@ */ import expect from '@kbn/expect'; -import { EntityType } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/common.gen'; + import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { cleanEngines } from '../../utils'; +import { EntityStoreUtils } from '../../utils'; export default ({ getService }: FtrProviderContext) => { const api = getService('securitySolutionApi'); - const es = getService('es'); - - const initEntityEngineForEntityType = async (entityType: EntityType) => { - return api - .initEntityEngine({ - params: { entityType }, - body: {}, - }) - .expect(200); - }; - - const expectTransformExists = async (transformId: string) => { - return expectTransformStatus(transformId, true); - }; - - const expectTransformNotFound = async (transformId: string, attempts: number = 5) => { - return expectTransformStatus(transformId, false); - }; - - const expectTransformStatus = async ( - transformId: string, - exists: boolean, - attempts: number = 5, - delayMs: number = 2000 - ) => { - let currentAttempt = 1; - while (currentAttempt <= attempts) { - try { - await es.transform.getTransform({ transform_id: transformId }); - if (!exists) { - throw new Error(`Expected transform ${transformId} to not exist, but it does`); - } - return; // Transform exists, exit the loop - } catch (e) { - if (currentAttempt === attempts) { - if (exists) { - throw new Error(`Expected transform ${transformId} to exist, but it does not: ${e}`); - } else { - return; // Transform does not exist, exit the loop - } - } - await new Promise((resolve) => setTimeout(resolve, delayMs)); - currentAttempt++; - } - } - }; - - const expectTransformsExist = async (transformIds: string[]) => - Promise.all(transformIds.map((id) => expectTransformExists(id))); + + const utils = EntityStoreUtils(getService); describe('@ess @serverless @skipInServerlessMKI Entity Store Engine APIs', () => { before(async () => { - await cleanEngines({ getService }); + await utils.cleanEngines(); }); describe('init', () => { afterEach(async () => { - await cleanEngines({ getService }); + await utils.cleanEngines(); }); it('should have installed the expected user resources', async () => { - await initEntityEngineForEntityType('user'); + await utils.initEntityEngineForEntityType('user'); const expectedTransforms = [ 'entities-v1-history-ea_default_user_entity_store', 'entities-v1-latest-ea_default_user_entity_store', ]; - await expectTransformsExist(expectedTransforms); + await utils.expectTransformsExist(expectedTransforms); }); it('should have installed the expected host resources', async () => { - await initEntityEngineForEntityType('host'); + await utils.initEntityEngineForEntityType('host'); const expectedTransforms = [ 'entities-v1-history-ea_default_host_entity_store', 'entities-v1-latest-ea_default_host_entity_store', ]; - await expectTransformsExist(expectedTransforms); + await utils.expectTransformsExist(expectedTransforms); }); }); describe('get and list', () => { before(async () => { await Promise.all([ - initEntityEngineForEntityType('host'), - initEntityEngineForEntityType('user'), + utils.initEntityEngineForEntityType('host'), + utils.initEntityEngineForEntityType('user'), ]); }); after(async () => { - await cleanEngines({ getService }); + await utils.cleanEngines(); }); describe('get', () => { @@ -169,11 +122,11 @@ export default ({ getService }: FtrProviderContext) => { describe('start and stop', () => { before(async () => { - await initEntityEngineForEntityType('host'); + await utils.initEntityEngineForEntityType('host'); }); after(async () => { - await cleanEngines({ getService }); + await utils.cleanEngines(); }); it('should stop the entity engine', async () => { @@ -211,7 +164,7 @@ export default ({ getService }: FtrProviderContext) => { describe('delete', () => { it('should delete the host entity engine', async () => { - await initEntityEngineForEntityType('host'); + await utils.initEntityEngineForEntityType('host'); await api .deleteEntityEngine({ @@ -220,12 +173,12 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await expectTransformNotFound('entities-v1-history-ea_host_entity_store'); - await expectTransformNotFound('entities-v1-latest-ea_host_entity_store'); + await utils.expectTransformNotFound('entities-v1-history-ea_host_entity_store'); + await utils.expectTransformNotFound('entities-v1-latest-ea_host_entity_store'); }); it('should delete the user entity engine', async () => { - await initEntityEngineForEntityType('user'); + await utils.initEntityEngineForEntityType('user'); await api .deleteEntityEngine({ @@ -234,8 +187,8 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await expectTransformNotFound('entities-v1-history-ea_user_entity_store'); - await expectTransformNotFound('entities-v1-latest-ea_user_entity_store'); + await utils.expectTransformNotFound('entities-v1-history-ea_user_entity_store'); + await utils.expectTransformNotFound('entities-v1-latest-ea_user_entity_store'); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts new file mode 100644 index 0000000000000..112c8b8b21511 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts @@ -0,0 +1,234 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { v4 as uuidv4 } from 'uuid'; +import { FtrProviderContextWithSpaces } from '../../../../ftr_provider_context_with_spaces'; +import { EntityStoreUtils } from '../../utils'; +export default ({ getService }: FtrProviderContextWithSpaces) => { + const api = getService('securitySolutionApi'); + const spaces = getService('spaces'); + const namespace = uuidv4().substring(0, 8); + + const utils = EntityStoreUtils(getService, namespace); + + describe('@ess Entity Store Engine APIs in non-default space', () => { + before(async () => { + await utils.cleanEngines(); + await spaces.create({ + id: namespace, + name: namespace, + disabledFeatures: [], + }); + }); + + after(async () => { + await spaces.delete(namespace); + }); + + describe('init', () => { + afterEach(async () => { + await utils.cleanEngines(); + }); + + it('should have installed the expected user resources', async () => { + await utils.initEntityEngineForEntityType('user'); + + const expectedTransforms = [ + `entities-v1-history-ea_${namespace}_user_entity_store`, + `entities-v1-latest-ea_${namespace}_user_entity_store`, + ]; + + await utils.expectTransformsExist(expectedTransforms); + }); + + it('should have installed the expected host resources', async () => { + await utils.initEntityEngineForEntityType('host'); + + const expectedTransforms = [ + `entities-v1-history-ea_${namespace}_host_entity_store`, + `entities-v1-latest-ea_${namespace}_host_entity_store`, + ]; + + await utils.expectTransformsExist(expectedTransforms); + }); + }); + + describe('get and list', () => { + before(async () => { + await Promise.all([ + utils.initEntityEngineForEntityType('host'), + utils.initEntityEngineForEntityType('user'), + ]); + }); + + after(async () => { + await utils.cleanEngines(); + }); + + describe('get', () => { + it('should return the host entity engine', async () => { + const getResponse = await api + .getEntityEngine( + { + params: { entityType: 'host' }, + }, + namespace + ) + .expect(200); + + expect(getResponse.body).to.eql({ + status: 'started', + type: 'host', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }); + }); + + it('should return the user entity engine', async () => { + const getResponse = await api + .getEntityEngine( + { + params: { entityType: 'user' }, + }, + namespace + ) + .expect(200); + + expect(getResponse.body).to.eql({ + status: 'started', + type: 'user', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }); + }); + }); + + describe('list', () => { + it('should return the list of entity engines', async () => { + const { body } = await api.listEntityEngines(namespace).expect(200); + + // @ts-expect-error body is any + const sortedEngines = body.engines.sort((a, b) => a.type.localeCompare(b.type)); + + expect(sortedEngines).to.eql([ + { + status: 'started', + type: 'host', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }, + { + status: 'started', + type: 'user', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }, + ]); + }); + }); + }); + + describe('start and stop', () => { + before(async () => { + await utils.initEntityEngineForEntityType('host'); + }); + + after(async () => { + await utils.cleanEngines(); + }); + + it('should stop the entity engine', async () => { + await api + .stopEntityEngine( + { + params: { entityType: 'host' }, + }, + namespace + ) + .expect(200); + + const { body } = await api + .getEntityEngine( + { + params: { entityType: 'host' }, + }, + namespace + ) + .expect(200); + + expect(body.status).to.eql('stopped'); + }); + + it('should start the entity engine', async () => { + await api + .startEntityEngine( + { + params: { entityType: 'host' }, + }, + namespace + ) + .expect(200); + + const { body } = await api + .getEntityEngine( + { + params: { entityType: 'host' }, + }, + namespace + ) + .expect(200); + + expect(body.status).to.eql('started'); + }); + }); + + describe('delete', () => { + it('should delete the host entity engine', async () => { + await utils.initEntityEngineForEntityType('host'); + + await api + .deleteEntityEngine( + { + params: { entityType: 'host' }, + query: { data: true }, + }, + namespace + ) + .expect(200); + + await utils.expectTransformNotFound( + `entities-v1-history-ea_${namespace}_host_entity_store` + ); + await utils.expectTransformNotFound(`entities-v1-latest-ea_${namespace}_host_entity_store`); + }); + + it('should delete the user entity engine', async () => { + await utils.initEntityEngineForEntityType('user'); + + await api + .deleteEntityEngine( + { + params: { entityType: 'user' }, + query: { data: true }, + }, + namespace + ) + .expect(200); + + await utils.expectTransformNotFound( + `entities-v1-history-ea_${namespace}_user_entity_store` + ); + await utils.expectTransformNotFound(`entities-v1-latest-ea_${namespace}_user_entity_store`); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts index 6e730b465350a..ec9c7a8f1dcba 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Entity Analytics - Entity Store', function () { loadTestFile(require.resolve('./entities_list')); loadTestFile(require.resolve('./engine')); + loadTestFile(require.resolve('./engine_nondefault_spaces')); }); } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts index 3f0ba0698a494..9dc1807d7263c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts @@ -5,29 +5,95 @@ * 2.0. */ +import { EntityType } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/common.gen'; + import { FtrProviderContext } from '../../../../api_integration/ftr_provider_context'; -export const cleanEngines = async ({ - getService, -}: { - getService: FtrProviderContext['getService']; -}) => { - const log = getService('log'); +export const EntityStoreUtils = ( + getService: FtrProviderContext['getService'], + namespace?: string +) => { const api = getService('securitySolutionApi'); + const es = getService('es'); + const log = getService('log'); + + log.debug(`EntityStoreUtils namespace: ${namespace}`); + + const cleanEngines = async () => { + const { body } = await api.listEntityEngines(namespace).expect(200); - const { body } = await api.listEntityEngines().expect(200); + // @ts-expect-error body is any + const engineTypes = body.engines.map((engine) => engine.type); - // @ts-expect-error body is any - const engineTypes = body.engines.map((engine) => engine.type); + log.info(`Cleaning engines: ${engineTypes.join(', ')}`); + try { + await Promise.all( + engineTypes.map((entityType: 'user' | 'host') => + api.deleteEntityEngine({ params: { entityType }, query: { data: true } }, namespace) + ) + ); + } catch (e) { + log.warning(`Error deleting engines: ${e.message}`); + } + }; - log.info(`Cleaning engines: ${engineTypes.join(', ')}`); - try { - await Promise.all( - engineTypes.map((entityType: 'user' | 'host') => - api.deleteEntityEngine({ params: { entityType }, query: { data: true } }) + const initEntityEngineForEntityType = async (entityType: EntityType) => { + log.info(`Initializing engine for entity type ${entityType} in namespace ${namespace}`); + return api + .initEntityEngine( + { + params: { entityType }, + body: {}, + }, + namespace ) - ); - } catch (e) { - log.warning(`Error deleting engines: ${e.message}`); - } + .expect(200); + }; + + const expectTransformStatus = async ( + transformId: string, + exists: boolean, + attempts: number = 5, + delayMs: number = 2000 + ) => { + let currentAttempt = 1; + while (currentAttempt <= attempts) { + try { + await es.transform.getTransform({ transform_id: transformId }); + if (!exists) { + throw new Error(`Expected transform ${transformId} to not exist, but it does`); + } + return; // Transform exists, exit the loop + } catch (e) { + if (currentAttempt === attempts) { + if (exists) { + throw new Error(`Expected transform ${transformId} to exist, but it does not: ${e}`); + } else { + return; // Transform does not exist, exit the loop + } + } + await new Promise((resolve) => setTimeout(resolve, delayMs)); + currentAttempt++; + } + } + }; + + const expectTransformNotFound = async (transformId: string, attempts: number = 5) => { + return expectTransformStatus(transformId, false); + }; + const expectTransformExists = async (transformId: string) => { + return expectTransformStatus(transformId, true); + }; + + const expectTransformsExist = async (transformIds: string[]) => + Promise.all(transformIds.map((id) => expectTransformExists(id))); + + return { + cleanEngines, + initEntityEngineForEntityType, + expectTransformStatus, + expectTransformNotFound, + expectTransformExists, + expectTransformsExist, + }; };