diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/configurations.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/configurations.ts index 6dbf68c699fd5..610ada87f5159 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/configurations.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/configurations.ts @@ -129,6 +129,9 @@ export const riskScoreFieldMap: FieldMap = { } as const; export const mappingComponentName = '.risk-score-mappings'; +export const nameSpaceAwareMappingsComponentName = (namespace: string): string => { + return `${mappingComponentName}-${namespace}`; +}; export const totalFieldsLimit = 1000; export const getIndexPatternDataStream = (namespace: string): IIndexPatternString => ({ diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.test.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.test.ts index 2ddd04a766944..72c0e14f9913b 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.test.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.test.ts @@ -40,6 +40,7 @@ jest.spyOn(transforms, 'scheduleTransformNow').mockResolvedValue(Promise.resolve describe('RiskScoreDataClient', () => { let riskScoreDataClient: RiskScoreDataClient; + let riskScoreDataClientWithNameSpace: RiskScoreDataClient; let mockSavedObjectClient: ReturnType; let logger: ReturnType; const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; @@ -56,6 +57,8 @@ describe('RiskScoreDataClient', () => { namespace: 'default', }; riskScoreDataClient = new RiskScoreDataClient(options); + const optionsWithNamespace = { ...options, namespace: 'space-1' }; + riskScoreDataClientWithNameSpace = new RiskScoreDataClient(optionsWithNamespace); }); afterEach(() => { @@ -80,369 +83,396 @@ describe('RiskScoreDataClient', () => { }); describe('init success', () => { - it('should initialize risk engine resources', async () => { - await riskScoreDataClient.init(); + it('should initialize risk engine resources in the appropriate space', async () => { + const assertComponentTemplate = (namespace: string) => { + expect(createOrUpdateComponentTemplate).toHaveBeenCalledWith( + expect.objectContaining({ + logger, + esClient, + template: expect.objectContaining({ + name: `.risk-score-mappings-${namespace}`, + _meta: { + managed: true, + }, + }), + totalFieldsLimit: 1000, + }) + ); + }; - expect(createOrUpdateComponentTemplate).toHaveBeenCalledWith( - expect.objectContaining({ + const assertIndexTemplate = (namespace: string) => { + expect(createOrUpdateIndexTemplate).toHaveBeenCalledWith({ logger, esClient, - template: expect.objectContaining({ - name: '.risk-score-mappings', - _meta: { - managed: true, - }, - }), - totalFieldsLimit: 1000, - }) - ); - expect((createOrUpdateComponentTemplate as jest.Mock).mock.lastCall[0].template.template) - .toMatchInlineSnapshot(` - Object { - "mappings": Object { - "dynamic": "strict", - "properties": Object { - "@timestamp": Object { - "ignore_malformed": false, - "type": "date", + template: { + name: `.risk-score.risk-score-${namespace}-index-template`, + body: { + data_stream: { hidden: true }, + index_patterns: [`risk-score.risk-score-${namespace}`], + composed_of: [`.risk-score-mappings-${namespace}`], + template: { + lifecycle: {}, + settings: { + 'index.mapping.total_fields.limit': totalFieldsLimit, + }, + mappings: { + dynamic: false, + _meta: { + kibana: { + version: '8.9.0', + }, + managed: true, + namespace, }, - "host": Object { - "properties": Object { - "name": Object { - "type": "keyword", - }, - "risk": Object { - "properties": Object { - "calculated_level": Object { - "type": "keyword", - }, - "calculated_score": Object { - "type": "float", - }, - "calculated_score_norm": Object { - "type": "float", - }, - "category_1_count": Object { - "type": "long", - }, - "category_1_score": Object { - "type": "float", - }, - "id_field": Object { - "type": "keyword", - }, - "id_value": Object { - "type": "keyword", - }, - "inputs": Object { - "properties": Object { - "category": Object { - "type": "keyword", - }, - "description": Object { - "type": "keyword", - }, - "id": Object { - "type": "keyword", - }, - "index": Object { - "type": "keyword", - }, - "risk_score": Object { - "type": "float", - }, - "timestamp": Object { - "type": "date", - }, + }, + }, + _meta: { + kibana: { + version: '8.9.0', + }, + managed: true, + namespace, + }, + }, + }, + }); + }; + + const assertDataStream = (namespace: string) => { + expect(createDataStream).toHaveBeenCalledWith({ + logger, + esClient, + totalFieldsLimit, + indexPatterns: { + template: `.risk-score.risk-score-${namespace}-index-template`, + alias: `risk-score.risk-score-${namespace}`, + }, + }); + }; + + const assertIndex = (namespace: string) => { + expect(createOrUpdateIndex).toHaveBeenCalledWith({ + logger, + esClient, + options: { + index: `risk-score.risk-score-latest-${namespace}`, + mappings: { + dynamic: false, + properties: { + '@timestamp': { + ignore_malformed: false, + type: 'date', + }, + host: { + properties: { + name: { + type: 'keyword', + }, + risk: { + properties: { + calculated_level: { + type: 'keyword', + }, + calculated_score: { + type: 'float', + }, + calculated_score_norm: { + type: 'float', + }, + category_1_count: { + type: 'long', + }, + category_1_score: { + type: 'float', + }, + id_field: { + type: 'keyword', + }, + id_value: { + type: 'keyword', + }, + inputs: { + properties: { + category: { + type: 'keyword', + }, + description: { + type: 'keyword', + }, + id: { + type: 'keyword', + }, + index: { + type: 'keyword', + }, + risk_score: { + type: 'float', + }, + timestamp: { + type: 'date', }, - "type": "object", - }, - "notes": Object { - "type": "keyword", }, + type: 'object', + }, + notes: { + type: 'keyword', }, - "type": "object", }, + type: 'object', }, }, - "user": Object { - "properties": Object { - "name": Object { - "type": "keyword", - }, - "risk": Object { - "properties": Object { - "calculated_level": Object { - "type": "keyword", - }, - "calculated_score": Object { - "type": "float", - }, - "calculated_score_norm": Object { - "type": "float", - }, - "category_1_count": Object { - "type": "long", - }, - "category_1_score": Object { - "type": "float", - }, - "id_field": Object { - "type": "keyword", - }, - "id_value": Object { - "type": "keyword", - }, - "inputs": Object { - "properties": Object { - "category": Object { - "type": "keyword", - }, - "description": Object { - "type": "keyword", - }, - "id": Object { - "type": "keyword", - }, - "index": Object { - "type": "keyword", - }, - "risk_score": Object { - "type": "float", - }, - "timestamp": Object { - "type": "date", - }, + }, + user: { + properties: { + name: { + type: 'keyword', + }, + risk: { + properties: { + calculated_level: { + type: 'keyword', + }, + calculated_score: { + type: 'float', + }, + calculated_score_norm: { + type: 'float', + }, + category_1_count: { + type: 'long', + }, + category_1_score: { + type: 'float', + }, + id_field: { + type: 'keyword', + }, + id_value: { + type: 'keyword', + }, + inputs: { + properties: { + category: { + type: 'keyword', + }, + description: { + type: 'keyword', + }, + id: { + type: 'keyword', + }, + index: { + type: 'keyword', + }, + risk_score: { + type: 'float', + }, + timestamp: { + type: 'date', }, - "type": "object", - }, - "notes": Object { - "type": "keyword", }, + type: 'object', + }, + notes: { + type: 'keyword', }, - "type": "object", }, + type: 'object', }, }, }, }, - "settings": Object {}, - } - `); + }, + }, + }); + }; - expect(createOrUpdateIndexTemplate).toHaveBeenCalledWith({ - logger, - esClient, - template: { - name: '.risk-score.risk-score-default-index-template', - body: { - data_stream: { hidden: true }, - index_patterns: ['risk-score.risk-score-default'], - composed_of: ['.risk-score-mappings'], - template: { - lifecycle: {}, - settings: { - 'index.mapping.total_fields.limit': totalFieldsLimit, - }, - mappings: { - dynamic: false, - _meta: { - kibana: { - version: '8.9.0', - }, - managed: true, - namespace: 'default', - }, + const assertTransform = (namespace: string) => { + expect(transforms.createTransform).toHaveBeenCalledWith({ + logger, + esClient, + transform: { + dest: { + index: `risk-score.risk-score-latest-${namespace}`, + }, + frequency: '1h', + latest: { + sort: '@timestamp', + unique_key: ['host.name', 'user.name'], + }, + source: { + index: [`risk-score.risk-score-${namespace}`], + }, + sync: { + time: { + delay: '0s', + field: '@timestamp', }, }, + transform_id: `risk_score_latest_transform_${namespace}`, + settings: { + unattended: true, + }, _meta: { - kibana: { - version: '8.9.0', - }, + version: 2, managed: true, - namespace: 'default', + managed_by: 'security-entity-analytics', }, }, - }, - }); + }); + }; - expect(createDataStream).toHaveBeenCalledWith({ - logger, - esClient, - totalFieldsLimit, - indexPatterns: { - template: `.risk-score.risk-score-default-index-template`, - alias: `risk-score.risk-score-default`, - }, - }); + // Default namespace + esClient.cluster.existsComponentTemplate.mockResolvedValue(false); + await riskScoreDataClient.init(); + assertComponentTemplate('default'); + assertIndexTemplate('default'); + assertDataStream('default'); + assertIndex('default'); + assertTransform('default'); - expect(createOrUpdateIndex).toHaveBeenCalledWith({ - logger, - esClient, - options: { - index: `risk-score.risk-score-latest-default`, - mappings: { - dynamic: false, - properties: { - '@timestamp': { - ignore_malformed: false, - type: 'date', - }, - host: { - properties: { - name: { - type: 'keyword', - }, - risk: { - properties: { - calculated_level: { - type: 'keyword', - }, - calculated_score: { - type: 'float', - }, - calculated_score_norm: { - type: 'float', - }, - category_1_count: { - type: 'long', - }, - category_1_score: { - type: 'float', - }, - id_field: { - type: 'keyword', - }, - id_value: { - type: 'keyword', - }, - inputs: { - properties: { - category: { - type: 'keyword', - }, - description: { - type: 'keyword', - }, - id: { - type: 'keyword', - }, - index: { - type: 'keyword', - }, - risk_score: { - type: 'float', - }, - timestamp: { - type: 'date', + // Space-1 namespace + esClient.cluster.existsComponentTemplate.mockResolvedValue(false); + await riskScoreDataClientWithNameSpace.init(); + assertComponentTemplate('space-1'); + assertIndexTemplate('space-1'); + assertDataStream('space-1'); + assertIndex('space-1'); + assertTransform('space-1'); + + expect((createOrUpdateComponentTemplate as jest.Mock).mock.lastCall[0].template.template) + .toMatchInlineSnapshot(` + Object { + "mappings": Object { + "dynamic": "strict", + "properties": Object { + "@timestamp": Object { + "ignore_malformed": false, + "type": "date", + }, + "host": Object { + "properties": Object { + "name": Object { + "type": "keyword", + }, + "risk": Object { + "properties": Object { + "calculated_level": Object { + "type": "keyword", + }, + "calculated_score": Object { + "type": "float", + }, + "calculated_score_norm": Object { + "type": "float", + }, + "category_1_count": Object { + "type": "long", + }, + "category_1_score": Object { + "type": "float", + }, + "id_field": Object { + "type": "keyword", + }, + "id_value": Object { + "type": "keyword", + }, + "inputs": Object { + "properties": Object { + "category": Object { + "type": "keyword", + }, + "description": Object { + "type": "keyword", + }, + "id": Object { + "type": "keyword", + }, + "index": Object { + "type": "keyword", + }, + "risk_score": Object { + "type": "float", + }, + "timestamp": Object { + "type": "date", + }, }, + "type": "object", + }, + "notes": Object { + "type": "keyword", }, - type: 'object', - }, - notes: { - type: 'keyword', }, + "type": "object", }, - type: 'object', }, }, - }, - user: { - properties: { - name: { - type: 'keyword', - }, - risk: { - properties: { - calculated_level: { - type: 'keyword', - }, - calculated_score: { - type: 'float', - }, - calculated_score_norm: { - type: 'float', - }, - category_1_count: { - type: 'long', - }, - category_1_score: { - type: 'float', - }, - id_field: { - type: 'keyword', - }, - id_value: { - type: 'keyword', - }, - inputs: { - properties: { - category: { - type: 'keyword', - }, - description: { - type: 'keyword', - }, - id: { - type: 'keyword', - }, - index: { - type: 'keyword', - }, - risk_score: { - type: 'float', - }, - timestamp: { - type: 'date', + "user": Object { + "properties": Object { + "name": Object { + "type": "keyword", + }, + "risk": Object { + "properties": Object { + "calculated_level": Object { + "type": "keyword", + }, + "calculated_score": Object { + "type": "float", + }, + "calculated_score_norm": Object { + "type": "float", + }, + "category_1_count": Object { + "type": "long", + }, + "category_1_score": Object { + "type": "float", + }, + "id_field": Object { + "type": "keyword", + }, + "id_value": Object { + "type": "keyword", + }, + "inputs": Object { + "properties": Object { + "category": Object { + "type": "keyword", + }, + "description": Object { + "type": "keyword", + }, + "id": Object { + "type": "keyword", + }, + "index": Object { + "type": "keyword", + }, + "risk_score": Object { + "type": "float", + }, + "timestamp": Object { + "type": "date", + }, }, + "type": "object", + }, + "notes": Object { + "type": "keyword", }, - type: 'object', - }, - notes: { - type: 'keyword', }, + "type": "object", }, - type: 'object', }, }, }, }, - }, - }, - }); - - expect(transforms.createTransform).toHaveBeenCalledWith({ - logger, - esClient, - transform: { - dest: { - index: 'risk-score.risk-score-latest-default', - }, - frequency: '1h', - latest: { - sort: '@timestamp', - unique_key: ['host.name', 'user.name'], - }, - source: { - index: ['risk-score.risk-score-default'], - }, - sync: { - time: { - delay: '0s', - field: '@timestamp', - }, - }, - transform_id: 'risk_score_latest_transform_default', - settings: { - unattended: true, - }, - _meta: { - version: 2, - managed: true, - managed_by: 'security-entity-analytics', - }, - }, - }); + "settings": Object {}, + } + `); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts index 0b3c38f3602fd..23c4317de3b8c 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts @@ -22,6 +22,7 @@ import { getIndexPatternDataStream, getTransformOptions, mappingComponentName, + nameSpaceAwareMappingsComponentName, riskScoreFieldMap, totalFieldsLimit, } from './configurations'; @@ -114,12 +115,22 @@ export class RiskScoreDataClient { namespace, }; + // Check if there are any existing component templates with the namespace in the name + + const oldComponentTemplateExists = await esClient.cluster.existsComponentTemplate({ + name: mappingComponentName, + }); + if (oldComponentTemplateExists) { + await this.updateComponentTemplateNamewithNamespace(namespace); + } + + // Update the new component template with the required data await Promise.all([ createOrUpdateComponentTemplate({ logger: this.options.logger, esClient, template: { - name: mappingComponentName, + name: nameSpaceAwareMappingsComponentName(namespace), _meta: { managed: true, }, @@ -140,7 +151,7 @@ export class RiskScoreDataClient { body: { data_stream: { hidden: true }, index_patterns: [indexPatterns.alias], - composed_of: [mappingComponentName], + composed_of: [nameSpaceAwareMappingsComponentName(namespace)], template: { lifecycle: {}, settings: { @@ -156,6 +167,14 @@ export class RiskScoreDataClient { }, }); + // Delete the component template without the namespace in the name + await esClient.cluster.deleteComponentTemplate( + { + name: mappingComponentName, + }, + { ignore: [404] } + ); + await createDataStream({ logger: this.options.logger, esClient, @@ -286,4 +305,20 @@ export class RiskScoreDataClient { { logger: this.options.logger } ); } + + private async updateComponentTemplateNamewithNamespace(namespace: string): Promise { + const esClient = this.options.esClient; + const oldComponentTemplateResponse = await esClient.cluster.getComponentTemplate( + { + name: mappingComponentName, + }, + { ignore: [404] } + ); + const oldComponentTemplate = oldComponentTemplateResponse?.component_templates[0]; + const newComponentTemplateName = nameSpaceAwareMappingsComponentName(namespace); + await esClient.cluster.putComponentTemplate({ + name: newComponentTemplateName, + body: oldComponentTemplate.component_template, + }); + } } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts index f0b966c1c166e..532b415911cd2 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts @@ -303,7 +303,87 @@ export default ({ getService }: FtrProviderContext) => { ); }); - describe('remove legacy risk score transform', function () { + it('should update the existing component template and index template without any errors', async () => { + const componentTemplateName = '.risk-score-mappings'; + const indexTemplateName = '.risk-score.risk-score-default-index-template'; + const newComponentTemplateName = '.risk-score-mappings-default'; + + // Call API to put the component template and index template + + await es.cluster.putComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + properties: { + timestamp: { + type: 'date', + }, + user: { + properties: { + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + }, + }, + }, + }, + }, + version: 1, + }, + }); + + // Call an API to put the index template + + await es.indices.putIndexTemplate({ + name: indexTemplateName, + body: { + index_patterns: [indexTemplateName], + composed_of: [componentTemplateName], + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + properties: { + timestamp: { + type: 'date', + }, + user: { + properties: { + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + }, + }, + }, + }, + }, + }, + }); + + const response = await riskEngineRoutes.init(); + expect(response.status).to.eql(200); + expect(response.body.result.errors).to.eql([]); + + const response2 = await es.cluster.getComponentTemplate({ + name: newComponentTemplateName, + }); + expect(response2.component_templates.length).to.eql(1); + expect(response2.component_templates[0].name).to.eql(newComponentTemplateName); + }); + + // Failing: See https://github.com/elastic/kibana/issues/191637 + describe.skip('remove legacy risk score transform', function () { this.tags('skipFIPS'); it('should remove legacy risk score transform if it exists', async () => { await installLegacyRiskScore({ supertest });