Skip to content

Commit

Permalink
[Security Solution][Entity Analytics] Scoping the entity store to spa…
Browse files Browse the repository at this point in the history
…ces (elastic#193303)

## Summary

This PR introduces Kibana Spaces support for the Entity Store.
It implements elastic/security-team#10530




### How to test

1. Add some host/user data
* Easiest is to use
[elastic/security-data-generator](https://github.com/elastic/security-documents-generator)
2. Make sure to add `entityStoreEnabled` under
`xpack.securitySolution.enableExperimental` in your `kibana.dev.yml`
3. Make sure to create a second space other than `default`, either via
the UI or the spaces API.
4. In the default space kibana dev tools, call the `POST
kbn:/api/entity_store/engines/{entity_type}/init {}` route for either
`user` or `host`.
5. Switch to the other space and call `INIT` again.
6. Check that calling the `GET kbn:api/entity_store/engines` route in
each space returns only one engine.
7. Check that calling `GET
/.kibana*/_search?q=type:entity-engine-status` returns 2 engines, one in
each space.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
tiansivive and kibanamachine authored Sep 23, 2024
1 parent 9e2ad98 commit 16dcfa8
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 66 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

import { entityDefinitionSchema, type EntityDefinition } from '@kbn/entities-schema';
import { ENTITY_STORE_DEFAULT_SOURCE_INDICES } from './constants';
import { getEntityDefinitionId } from './utils/utils';
import { buildEntityDefinitionId } from './utils/utils';

export const buildHostEntityDefinition = (): EntityDefinition =>
export const buildHostEntityDefinition = (space: string): EntityDefinition =>
entityDefinitionSchema.parse({
id: getEntityDefinitionId('host'),
id: buildEntityDefinitionId('host', space),
name: 'EA Host Store',
type: 'host',
indexPatterns: ENTITY_STORE_DEFAULT_SOURCE_INDICES,
Expand All @@ -34,22 +34,14 @@ export const buildHostEntityDefinition = (): EntityDefinition =>
version: '1.0.0',
});

export const buildUserEntityDefinition = (): EntityDefinition =>
export const buildUserEntityDefinition = (space: string): EntityDefinition =>
entityDefinitionSchema.parse({
id: getEntityDefinitionId('user'),
id: buildEntityDefinitionId('user', space),
name: 'EA User Store',
indexPatterns: ENTITY_STORE_DEFAULT_SOURCE_INDICES,
identityFields: ['user.name'],
displayNameTemplate: '{{user.name}}',
metadata: [
'user.domain',
'user.email',
'user.full_name',
'user.hash',
'user.id',
'user.name',
'user.roles',
],
metadata: ['user.email', 'user.full_name', 'user.hash', 'user.id', 'user.name', 'user.roles'],
history: {
timestampField: '@timestamp',
interval: '1m',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ describe('EntityStoreDataClient', () => {
expect(esClientMock.search).toHaveBeenCalledWith(
expect.objectContaining({
index: [
'.entities.v1.latest.ea_host_entity_store',
'.entities.v1.latest.ea_user_entity_store',
'.entities.v1.latest.ea_default_host_entity_store',
'.entities.v1.latest.ea_default_user_entity_store',
],
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import type {
InitEntityStoreRequestBody,
InitEntityStoreResponse,
} from '../../../../common/api/entity_analytics/entity_store/engine/init.gen';

import type {
EngineDescriptor,
EntityType,
InspectQuery,
} from '../../../../common/api/entity_analytics/entity_store/common.gen';
import { entityEngineDescriptorTypeName } from './saved_object';

import { EngineDescriptorClient } from './saved_object/engine_descriptor';
import { getEntitiesIndexName, getEntityDefinition } from './utils/utils';
import { ENGINE_STATUS, MAX_SEARCH_RESPONSE_SIZE } from './constants';
Expand All @@ -45,14 +45,17 @@ interface SearchEntitiesParams {
export class EntityStoreDataClient {
private engineClient: EngineDescriptorClient;
constructor(private readonly options: EntityStoreClientOpts) {
this.engineClient = new EngineDescriptorClient(options.soClient);
this.engineClient = new EngineDescriptorClient({
soClient: options.soClient,
namespace: options.namespace,
});
}

public async init(
entityType: EntityType,
{ indexPattern = '', filter = '' }: InitEntityStoreRequestBody
): Promise<InitEntityStoreResponse> {
const definition = getEntityDefinition(entityType);
const definition = getEntityDefinition(entityType, this.options.namespace);

this.options.logger.info(`Initializing entity store for ${entityType}`);

Expand All @@ -72,7 +75,7 @@ export class EntityStoreDataClient {
}

public async start(entityType: EntityType) {
const definition = getEntityDefinition(entityType);
const definition = getEntityDefinition(entityType, this.options.namespace);

const descriptor = await this.engineClient.get(entityType);

Expand All @@ -89,7 +92,7 @@ export class EntityStoreDataClient {
}

public async stop(entityType: EntityType) {
const definition = getEntityDefinition(entityType);
const definition = getEntityDefinition(entityType, this.options.namespace);

const descriptor = await this.engineClient.get(entityType);

Expand All @@ -110,18 +113,11 @@ export class EntityStoreDataClient {
}

public async list() {
return this.options.soClient
.find<EngineDescriptor>({
type: entityEngineDescriptorTypeName,
})
.then(({ saved_objects: engines }) => ({
engines: engines.map((engine) => engine.attributes),
count: engines.length,
}));
return this.engineClient.list();
}

public async delete(entityType: EntityType, deleteData: boolean) {
const { id } = getEntityDefinition(entityType);
const { id } = getEntityDefinition(entityType, this.options.namespace);

this.options.logger.info(`Deleting entity store for ${entityType}`);

Expand All @@ -138,7 +134,7 @@ export class EntityStoreDataClient {
}> {
const { page, perPage, sortField, sortOrder, filterQuery, entityTypes } = params;

const index = entityTypes.map(getEntitiesIndexName);
const index = entityTypes.map((type) => getEntitiesIndexName(type, this.options.namespace));
const from = (page - 1) * perPage;
const sort = sortField ? [{ [sortField]: sortOrder }] : undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,21 @@ import { entityEngineDescriptorTypeName } from './engine_descriptor_type';
import { getByEntityTypeQuery, getEntityDefinition } from '../utils/utils';
import { ENGINE_STATUS } from '../constants';

interface EngineDescriptorDependencies {
soClient: SavedObjectsClientContract;
namespace: string;
}

export class EngineDescriptorClient {
constructor(private readonly soClient: SavedObjectsClientContract) {}
constructor(private readonly deps: EngineDescriptorDependencies) {}

async init(entityType: EntityType, definition: EntityDefinition, filter: string) {
const engineDescriptor = await this.find(entityType);

if (engineDescriptor.total > 0)
throw new Error(`Entity engine for ${entityType} already exists`);

const { attributes } = await this.soClient.create<EngineDescriptor>(
const { attributes } = await this.deps.soClient.create<EngineDescriptor>(
entityEngineDescriptorTypeName,
{
status: ENGINE_STATUS.INSTALLING,
Expand All @@ -43,7 +48,7 @@ export class EngineDescriptorClient {
}

async update(id: string, status: EngineStatus) {
const { attributes } = await this.soClient.update<EngineDescriptor>(
const { attributes } = await this.deps.soClient.update<EngineDescriptor>(
entityEngineDescriptorTypeName,
id,
{ status },
Expand All @@ -53,24 +58,37 @@ export class EngineDescriptorClient {
}

async find(entityType: EntityType): Promise<SavedObjectsFindResponse<EngineDescriptor>> {
return this.soClient.find<EngineDescriptor>({
return this.deps.soClient.find<EngineDescriptor>({
type: entityEngineDescriptorTypeName,
filter: getByEntityTypeQuery(entityType),
namespaces: [this.deps.namespace],
});
}

async get(entityType: EntityType): Promise<EngineDescriptor> {
const { id } = getEntityDefinition(entityType);
const { id } = getEntityDefinition(entityType, this.deps.namespace);

const { attributes } = await this.soClient.get<EngineDescriptor>(
const { attributes } = await this.deps.soClient.get<EngineDescriptor>(
entityEngineDescriptorTypeName,
id
);

return attributes;
}

async list() {
return this.deps.soClient
.find<EngineDescriptor>({
type: entityEngineDescriptorTypeName,
namespaces: [this.deps.namespace],
})
.then(({ saved_objects: engines }) => ({
engines: engines.map((engine) => engine.attributes),
count: engines.length,
}));
}

async delete(id: string) {
return this.soClient.delete(entityEngineDescriptorTypeName, id);
return this.deps.soClient.delete(entityEngineDescriptorTypeName, id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,34 @@
* 2.0.
*/

import type { SavedObjectsFindResponse } from '@kbn/core-saved-objects-api-server';

import {
ENTITY_LATEST,
ENTITY_SCHEMA_VERSION_V1,
entitiesIndexPattern,
} from '@kbn/entities-schema';
import type {
EngineDescriptor,
EntityType,
} from '../../../../../common/api/entity_analytics/entity_store/common.gen';
import type { EntityType } from '../../../../../common/api/entity_analytics/entity_store/common.gen';
import { buildHostEntityDefinition, buildUserEntityDefinition } from '../definition';

import { entityEngineDescriptorTypeName } from '../saved_object';

export const getEntityDefinition = (entityType: EntityType) => {
if (entityType === 'host') return buildHostEntityDefinition();
if (entityType === 'user') return buildUserEntityDefinition();
export const getEntityDefinition = (entityType: EntityType, space: string) => {
if (entityType === 'host') return buildHostEntityDefinition(space);
if (entityType === 'user') return buildUserEntityDefinition(space);

throw new Error(`Unsupported entity type: ${entityType}`);
};

export const ensureEngineExists =
(entityType: EntityType) => (results: SavedObjectsFindResponse<EngineDescriptor>) => {
if (results.total === 0) {
throw new Error(`Entity engine for ${entityType} does not exist`);
}
return results.saved_objects[0].attributes;
};

export const getByEntityTypeQuery = (entityType: EntityType) => {
return `${entityEngineDescriptorTypeName}.attributes.type: ${entityType}`;
};

export const getEntitiesIndexName = (entityType: EntityType) =>
export const getEntitiesIndexName = (entityType: EntityType, namespace: string) =>
entitiesIndexPattern({
schemaVersion: ENTITY_SCHEMA_VERSION_V1,
dataset: ENTITY_LATEST,
definitionId: getEntityDefinitionId(entityType),
definitionId: buildEntityDefinitionId(entityType, namespace),
});

export const getEntityDefinitionId = (entityType: EntityType) => `ea_${entityType}_entity_store`;
export const buildEntityDefinitionId = (entityType: EntityType, space: string) => {
return `ea_${space}_${entityType}_entity_store`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"type": "doc",
"value": {
"id": "a4cf452c1e0375c3d4412cb550ad1783358468a3b3b777da4829d72c7d6fb74f",
"index": ".entities.v1.latest.ea_user_entity_store",
"index": ".entities.v1.latest.ea_default_user_entity_store",
"source": {
"event": {
"ingested": "2024-09-11T11:26:49.706875Z"
Expand All @@ -27,7 +27,7 @@
"id": "LBQAgKHGmpup0Kg9nlKmeQ==",
"type": "node",
"firstSeenTimestamp": "2024-09-11T10:46:00.000Z",
"definitionId": "ea_user_entity_store"
"definitionId": "ea_default_user_entity_store"
}
}
}
Expand All @@ -37,7 +37,7 @@
"type": "doc",
"value": {
"id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb71f",
"index": ".entities.v1.latest.ea_host_entity_store",
"index": ".entities.v1.latest.ea_default_host_entity_store",
"source": {
"event": {
"ingested": "2024-09-11T11:26:49.641707Z"
Expand Down Expand Up @@ -78,7 +78,7 @@
"id": "ZXKm6GEcUJY6NHkMgPPmGQ==",
"type": "node",
"firstSeenTimestamp": "2024-09-11T10:46:00.000Z",
"definitionId": "ea_host_entity_store"
"definitionId": "ea_default_host_entity_store"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "index",
"value": {
"index": ".entities.v1.latest.ea_host_entity_store",
"index": ".entities.v1.latest.ea_default_host_entity_store",
"mappings": {
"date_detection": false,
"dynamic_templates": [
Expand Down Expand Up @@ -162,7 +162,7 @@
{
"type": "index",
"value": {
"index": ".entities.v1.latest.ea_user_entity_store",
"index": ".entities.v1.latest.ea_default_user_entity_store",
"mappings": {
"date_detection": false,
"dynamic_templates": [
Expand Down

0 comments on commit 16dcfa8

Please sign in to comment.