diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.test.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.test.ts new file mode 100644 index 0000000000000..bd39257ac72e7 --- /dev/null +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.test.ts @@ -0,0 +1,183 @@ +/* + * 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 { EntityV2 } from '@kbn/entities-schema'; +import { instanceAsFilter } from './instance_as_filter'; +import { readSourceDefinitions } from './source_definition'; +import { loggerMock } from '@kbn/logging-mocks'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { EntitySourceDefinition } from '../types'; +import { UnknownEntityType } from '../errors/unknown_entity_type'; +import { InvalidEntityInstance } from '../errors/invalid_entity_instance'; + +const readSourceDefinitionsMock = readSourceDefinitions as jest.Mock; +jest.mock('./source_definition', () => ({ + readSourceDefinitions: jest.fn(), +})); +const esClientMock = elasticsearchServiceMock.createClusterClient(); +const logger = loggerMock.create(); + +describe('instanceAsFilter', () => { + it('throws if no sources are found for the type', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + }; + + const sources: EntitySourceDefinition[] = []; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).rejects.toThrowError( + UnknownEntityType + ); + }); + + it('throws if the instance cannot match any sources due to missing identity fields', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source', + type_id: 'my_type', + identity_fields: ['host.name'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).rejects.toThrowError( + InvalidEntityInstance + ); + }); + + it('creates a single source filter for a single identity field', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + 'host.name': 'my_host', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source', + type_id: 'my_type', + identity_fields: ['host.name'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).resolves.toBe( + '(host.name: "my_host")' + ); + }); + + it('creates a single source filter for multiple identity field', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + 'host.name': 'my_host', + 'host.os': 'my_os', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source', + type_id: 'my_type', + identity_fields: ['host.name', 'host.os'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).resolves.toBe( + '(host.name: "my_host" AND host.os: "my_os")' + ); + }); + + it('creates multiple source filters for a single identity field', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + 'host.name': 'my_host', + 'host.os': 'my_os', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source_host', + type_id: 'my_type', + identity_fields: ['host.name'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + { + id: 'my_source_os', + type_id: 'my_type', + identity_fields: ['host.os'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).resolves.toBe( + '(host.name: "my_host") OR (host.os: "my_os")' + ); + }); + + it('creates multiple source filters for multiple identity field', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + 'host.name': 'my_host', + 'host.os': 'my_os', + 'host.arch': 'my_arch', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source_host', + type_id: 'my_type', + identity_fields: ['host.name', 'host.arch'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + { + id: 'my_source_os', + type_id: 'my_type', + identity_fields: ['host.os', 'host.arch'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).resolves.toBe( + '(host.name: "my_host" AND host.arch: "my_arch") OR (host.os: "my_os" AND host.arch: "my_arch")' + ); + }); +}); diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.ts new file mode 100644 index 0000000000000..c936277db8e25 --- /dev/null +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.ts @@ -0,0 +1,56 @@ +/* + * 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 { EntityV2 } from '@kbn/entities-schema'; +import { Logger } from '@kbn/core/server'; +import { compact } from 'lodash'; +import { readSourceDefinitions } from './source_definition'; +import { InternalClusterClient } from '../types'; +import { UnknownEntityType } from '../errors/unknown_entity_type'; +import { InvalidEntityInstance } from '../errors/invalid_entity_instance'; + +export async function instanceAsFilter( + instance: EntityV2, + clusterClient: InternalClusterClient, + logger: Logger +) { + const sources = await readSourceDefinitions(clusterClient, logger, { + type: instance['entity.type'], + }); + + if (sources.length === 0) { + throw new UnknownEntityType(`No sources found for type ${instance['entity.type']}`); + } + + const sourceFilters = compact( + sources.map((source) => { + const { identity_fields: identityFields } = source; + + const instanceHasRequiredFields = identityFields.every((identityField) => + instance[identityField] ? true : false + ); + + if (!instanceHasRequiredFields) { + return undefined; + } + + const fieldFilters = identityFields.map( + (identityField) => `${identityField}: "${instance[identityField]}"` + ); + + return `(${fieldFilters.join(' AND ')})`; + }) + ); + + if (sourceFilters.length === 0) { + throw new InvalidEntityInstance( + `Entity ${instance['entity.id']} of type ${instance['entity.type']} is missing some identity fields, no sources could match` + ); + } + + return sourceFilters.join(' OR '); +} diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/errors/invalid_entity_instance.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/errors/invalid_entity_instance.ts new file mode 100644 index 0000000000000..12b0a94fe3ebb --- /dev/null +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/errors/invalid_entity_instance.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export class InvalidEntityInstance extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidEntityInstance'; + } +} diff --git a/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts b/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts index fed5b1c4df458..8799c7f365bf7 100644 --- a/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts +++ b/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts @@ -40,11 +40,15 @@ import { READ_ENTITIES_PRIVILEGE, } from './lib/v2/constants'; import { installBuiltInDefinitions } from './lib/v2/definitions/install_built_in_definitions'; +import { instanceAsFilter } from './lib/v2/definitions/instance_as_filter'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface EntityManagerServerPluginSetup {} export interface EntityManagerServerPluginStart { getScopedClient: (options: { request: KibanaRequest }) => Promise; + v2: { + instanceAsFilter: typeof instanceAsFilter; + }; } export const config: PluginConfigDescriptor = { @@ -197,6 +201,9 @@ export class EntityManagerServerPlugin getScopedClient: async ({ request }: { request: KibanaRequest }) => { return this.getScopedClient({ request, coreStart: core }); }, + v2: { + instanceAsFilter, + }, }; } diff --git a/x-pack/platform/plugins/shared/entity_manager/tsconfig.json b/x-pack/platform/plugins/shared/entity_manager/tsconfig.json index beb8097502b2b..0fc46870ea472 100644 --- a/x-pack/platform/plugins/shared/entity_manager/tsconfig.json +++ b/x-pack/platform/plugins/shared/entity_manager/tsconfig.json @@ -38,5 +38,6 @@ "@kbn/es-types", "@kbn/apm-utils", "@kbn/features-plugin", + "@kbn/core-elasticsearch-server-mocks", ] }