Skip to content

Commit

Permalink
[8.x] [EEM] Add instanceAsFilter helper (#204319) (#204544)
Browse files Browse the repository at this point in the history
# Backport

This will backport the following commits from `main` to `8.x`:
- [[EEM] Add instanceAsFilter helper
(#204319)](#204319)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Milton
Hultgren","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-12-17T11:03:16Z","message":"[EEM]
Add instanceAsFilter helper (#204319)\n\n## Summary\r\n\r\nThis PR adds
a new function on the entityManager's plugin start\r\ncontract,
`instanceAsFilter`, which translates an entity instance into a\r\nKQL
filter by doing a look up of the sources for the entity's
type.\r\n\r\nThe usage is as simple as this:\r\n```\r\nconst kql =
entityManager.v2.instanceAsFilter(entityInstance, clusterClient,
logger);\r\n```\r\n\r\nThis can be used as a filter in a query request
or as the input for
a\r\n[Filters\r\naggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filters-aggregation.html)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"3d8ac4908db4837970969543386709534952eaa0","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","Feature:EEM"],"title":"[EEM]
Add instanceAsFilter
helper","number":204319,"url":"https://github.com/elastic/kibana/pull/204319","mergeCommit":{"message":"[EEM]
Add instanceAsFilter helper (#204319)\n\n## Summary\r\n\r\nThis PR adds
a new function on the entityManager's plugin start\r\ncontract,
`instanceAsFilter`, which translates an entity instance into a\r\nKQL
filter by doing a look up of the sources for the entity's
type.\r\n\r\nThe usage is as simple as this:\r\n```\r\nconst kql =
entityManager.v2.instanceAsFilter(entityInstance, clusterClient,
logger);\r\n```\r\n\r\nThis can be used as a filter in a query request
or as the input for
a\r\n[Filters\r\naggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filters-aggregation.html)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"3d8ac4908db4837970969543386709534952eaa0"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/204319","number":204319,"mergeCommit":{"message":"[EEM]
Add instanceAsFilter helper (#204319)\n\n## Summary\r\n\r\nThis PR adds
a new function on the entityManager's plugin start\r\ncontract,
`instanceAsFilter`, which translates an entity instance into a\r\nKQL
filter by doing a look up of the sources for the entity's
type.\r\n\r\nThe usage is as simple as this:\r\n```\r\nconst kql =
entityManager.v2.instanceAsFilter(entityInstance, clusterClient,
logger);\r\n```\r\n\r\nThis can be used as a filter in a query request
or as the input for
a\r\n[Filters\r\naggregation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filters-aggregation.html)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"3d8ac4908db4837970969543386709534952eaa0"}}]}]
BACKPORT-->

Co-authored-by: Milton Hultgren <[email protected]>
  • Loading branch information
kibanamachine and miltonhultgren authored Dec 17, 2024
1 parent ab810a0 commit 0f80536
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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")'
);
});
});
Original file line number Diff line number Diff line change
@@ -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<string>(
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 ');
}
Original file line number Diff line number Diff line change
@@ -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';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<EntityClient>;
v2: {
instanceAsFilter: typeof instanceAsFilter;
};
}

export const config: PluginConfigDescriptor<EntityManagerConfig> = {
Expand Down Expand Up @@ -197,6 +201,9 @@ export class EntityManagerServerPlugin
getScopedClient: async ({ request }: { request: KibanaRequest }) => {
return this.getScopedClient({ request, coreStart: core });
},
v2: {
instanceAsFilter,
},
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@
"@kbn/es-types",
"@kbn/apm-utils",
"@kbn/features-plugin",
"@kbn/core-elasticsearch-server-mocks",
]
}

0 comments on commit 0f80536

Please sign in to comment.