[Entity Analytics] [Entity Store] Refactor entity store enablement (server side) (#199638)

## Summary

This PR adds 2 new endpoints regarding enablement of the Entity Store:
* `api/entity_store/enable`, which initializes entity engines for both
`user` and `host` entities
* `api/entity_store/status`, which computes a global store status based
on the individual engine status

In addition, running initialization of multiple engines in parallel is
now allowed.


### How to test

1. Use dev tools to call `POST kbn:/api/entity_store/enable`
2. Check that two engines were created and that the status is
`installing` by calling `GET kbn:/api/entity_store/status`
3. Wait a few seconds and keep calling the `status` endpoint. Once
initialization finishes, the status should switch to `running`

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Tiago Vila Verde 2024-11-19 14:11:24 +01:00 committed by GitHub
parent 6e9520aca2
commit 3757e64127
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 697 additions and 7 deletions

View file

@ -7395,6 +7395,43 @@ paths:
tags:
- Security Endpoint Management API
x-beta: true
/api/entity_store/enable:
post:
operationId: InitEntityStore
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
fieldHistoryLength:
default: 10
description: The number of historical values to keep for each field.
type: integer
filter:
type: string
indexPattern:
$ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern'
description: Schema for the entity store initialization
required: true
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
engines:
items:
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor'
type: array
succeeded:
type: boolean
description: Successful response
summary: Initialize the Entity Store
tags:
- Security Entity Analytics API
x-beta: true
/api/entity_store/engines:
get:
operationId: ListEntityEngines
@ -7713,6 +7750,27 @@ paths:
tags:
- Security Entity Analytics API
x-beta: true
/api/entity_store/status:
get:
operationId: GetEntityStoreStatus
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
engines:
items:
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor'
type: array
status:
$ref: '#/components/schemas/Security_Entity_Analytics_API_StoreStatus'
description: Successful response
summary: Get the status of the Entity Store
tags:
- Security Entity Analytics API
x-beta: true
/api/exception_lists:
delete:
description: Delete an exception list using the `id` or `list_id` field.
@ -45880,6 +45938,14 @@ components:
- index
- description
- category
Security_Entity_Analytics_API_StoreStatus:
enum:
- not_installed
- installing
- running
- stopped
- error
type: string
Security_Entity_Analytics_API_TaskManagerUnavailableResponse:
description: Task manager is unavailable
type: object

View file

@ -10282,6 +10282,42 @@ paths:
summary: Create or update a protection updates note
tags:
- Security Endpoint Management API
/api/entity_store/enable:
post:
operationId: InitEntityStore
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
fieldHistoryLength:
default: 10
description: The number of historical values to keep for each field.
type: integer
filter:
type: string
indexPattern:
$ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern'
description: Schema for the entity store initialization
required: true
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
engines:
items:
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor'
type: array
succeeded:
type: boolean
description: Successful response
summary: Initialize the Entity Store
tags:
- Security Entity Analytics API
/api/entity_store/engines:
get:
operationId: ListEntityEngines
@ -10591,6 +10627,26 @@ paths:
summary: List Entity Store Entities
tags:
- Security Entity Analytics API
/api/entity_store/status:
get:
operationId: GetEntityStoreStatus
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
type: object
properties:
engines:
items:
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor'
type: array
status:
$ref: '#/components/schemas/Security_Entity_Analytics_API_StoreStatus'
description: Successful response
summary: Get the status of the Entity Store
tags:
- Security Entity Analytics API
/api/exception_lists:
delete:
description: Delete an exception list using the `id` or `list_id` field.
@ -53601,6 +53657,14 @@ components:
- index
- description
- category
Security_Entity_Analytics_API_StoreStatus:
enum:
- not_installed
- installing
- running
- stopped
- error
type: string
Security_Entity_Analytics_API_TaskManagerUnavailableResponse:
description: Task manager is unavailable
type: object

View file

@ -39,6 +39,11 @@ export const EngineDescriptor = z.object({
error: z.object({}).optional(),
});
export type StoreStatus = z.infer<typeof StoreStatus>;
export const StoreStatus = z.enum(['not_installed', 'installing', 'running', 'stopped', 'error']);
export type StoreStatusEnum = typeof StoreStatus.enum;
export const StoreStatusEnum = StoreStatus.enum;
export type InspectQuery = z.infer<typeof InspectQuery>;
export const InspectQuery = z.object({
response: z.array(z.string()),

View file

@ -41,6 +41,15 @@ components:
- stopped
- updating
- error
StoreStatus:
type: string
enum:
- not_installed
- installing
- running
- stopped
- error
IndexPattern:
type: string

View file

@ -0,0 +1,42 @@
/*
* 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.
*/
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*
* info:
* title: Enable Entity Store
* version: 2023-10-31
*/
import { z } from '@kbn/zod';
import { IndexPattern, EngineDescriptor, StoreStatus } from './common.gen';
export type GetEntityStoreStatusResponse = z.infer<typeof GetEntityStoreStatusResponse>;
export const GetEntityStoreStatusResponse = z.object({
status: StoreStatus.optional(),
engines: z.array(EngineDescriptor).optional(),
});
export type InitEntityStoreRequestBody = z.infer<typeof InitEntityStoreRequestBody>;
export const InitEntityStoreRequestBody = z.object({
/**
* The number of historical values to keep for each field.
*/
fieldHistoryLength: z.number().int().optional().default(10),
indexPattern: IndexPattern.optional(),
filter: z.string().optional(),
});
export type InitEntityStoreRequestBodyInput = z.input<typeof InitEntityStoreRequestBody>;
export type InitEntityStoreResponse = z.infer<typeof InitEntityStoreResponse>;
export const InitEntityStoreResponse = z.object({
succeeded: z.boolean().optional(),
engines: z.array(EngineDescriptor).optional(),
});

View file

@ -0,0 +1,64 @@
openapi: 3.0.0
info:
title: Enable Entity Store
version: '2023-10-31'
paths:
/api/entity_store/enable:
post:
x-labels: [ess, serverless]
x-codegen-enabled: true
operationId: InitEntityStore
summary: Initialize the Entity Store
requestBody:
description: Schema for the entity store initialization
required: true
content:
application/json:
schema:
type: object
properties:
fieldHistoryLength:
type: integer
description: The number of historical values to keep for each field.
default: 10
indexPattern:
$ref: './common.schema.yaml#/components/schemas/IndexPattern'
filter:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
succeeded:
type: boolean
engines:
type: array
items:
$ref: './common.schema.yaml#/components/schemas/EngineDescriptor'
/api/entity_store/status:
get:
x-labels: [ess, serverless]
x-codegen-enabled: true
operationId: GetEntityStoreStatus
summary: Get the status of the Entity Store
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
status:
$ref: './common.schema.yaml#/components/schemas/StoreStatus'
engines:
type: array
items:
$ref: './common.schema.yaml#/components/schemas/EngineDescriptor'

View file

@ -231,6 +231,11 @@ import type {
InternalUploadAssetCriticalityRecordsResponse,
UploadAssetCriticalityRecordsResponse,
} from './entity_analytics/asset_criticality/upload_asset_criticality_csv.gen';
import type {
GetEntityStoreStatusResponse,
InitEntityStoreRequestBodyInput,
InitEntityStoreResponse,
} from './entity_analytics/entity_store/enablement.gen';
import type { ApplyEntityEngineDataviewIndicesResponse } from './entity_analytics/entity_store/engine/apply_dataview_indices.gen';
import type {
DeleteEntityEngineRequestQueryInput,
@ -1301,6 +1306,18 @@ finalize it.
})
.catch(catchAxiosErrorFormatAndThrow);
}
async getEntityStoreStatus() {
this.log.info(`${new Date().toISOString()} Calling API GetEntityStoreStatus`);
return this.kbnClient
.request<GetEntityStoreStatusResponse>({
path: '/api/entity_store/status',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
},
method: 'GET',
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Get all notes for a given document.
*/
@ -1529,6 +1546,19 @@ finalize it.
})
.catch(catchAxiosErrorFormatAndThrow);
}
async initEntityStore(props: InitEntityStoreProps) {
this.log.info(`${new Date().toISOString()} Calling API InitEntityStore`);
return this.kbnClient
.request<InitEntityStoreResponse>({
path: '/api/entity_store/enable',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
},
method: 'POST',
body: props.body,
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine
*/
@ -2290,6 +2320,9 @@ export interface InitEntityEngineProps {
params: InitEntityEngineRequestParamsInput;
body: InitEntityEngineRequestBodyInput;
}
export interface InitEntityStoreProps {
body: InitEntityStoreRequestBodyInput;
}
export interface InstallPrepackedTimelinesProps {
body: InstallPrepackedTimelinesRequestBodyInput;
}

View file

@ -267,6 +267,42 @@ paths:
summary: List asset criticality records
tags:
- Security Entity Analytics API
/api/entity_store/enable:
post:
operationId: InitEntityStore
requestBody:
content:
application/json:
schema:
type: object
properties:
fieldHistoryLength:
default: 10
description: The number of historical values to keep for each field.
type: integer
filter:
type: string
indexPattern:
$ref: '#/components/schemas/IndexPattern'
description: Schema for the entity store initialization
required: true
responses:
'200':
content:
application/json:
schema:
type: object
properties:
engines:
items:
$ref: '#/components/schemas/EngineDescriptor'
type: array
succeeded:
type: boolean
description: Successful response
summary: Initialize the Entity Store
tags:
- Security Entity Analytics API
/api/entity_store/engines:
get:
operationId: ListEntityEngines
@ -576,6 +612,26 @@ paths:
summary: List Entity Store Entities
tags:
- Security Entity Analytics API
/api/entity_store/status:
get:
operationId: GetEntityStoreStatus
responses:
'200':
content:
application/json:
schema:
type: object
properties:
engines:
items:
$ref: '#/components/schemas/EngineDescriptor'
type: array
status:
$ref: '#/components/schemas/StoreStatus'
description: Successful response
summary: Get the status of the Entity Store
tags:
- Security Entity Analytics API
/api/risk_score/engine/dangerously_delete_data:
delete:
description: >-
@ -1046,6 +1102,14 @@ components:
- index
- description
- category
StoreStatus:
enum:
- not_installed
- installing
- running
- stopped
- error
type: string
TaskManagerUnavailableResponse:
description: Task manager is unavailable
type: object

View file

@ -267,6 +267,42 @@ paths:
summary: List asset criticality records
tags:
- Security Entity Analytics API
/api/entity_store/enable:
post:
operationId: InitEntityStore
requestBody:
content:
application/json:
schema:
type: object
properties:
fieldHistoryLength:
default: 10
description: The number of historical values to keep for each field.
type: integer
filter:
type: string
indexPattern:
$ref: '#/components/schemas/IndexPattern'
description: Schema for the entity store initialization
required: true
responses:
'200':
content:
application/json:
schema:
type: object
properties:
engines:
items:
$ref: '#/components/schemas/EngineDescriptor'
type: array
succeeded:
type: boolean
description: Successful response
summary: Initialize the Entity Store
tags:
- Security Entity Analytics API
/api/entity_store/engines:
get:
operationId: ListEntityEngines
@ -576,6 +612,26 @@ paths:
summary: List Entity Store Entities
tags:
- Security Entity Analytics API
/api/entity_store/status:
get:
operationId: GetEntityStoreStatus
responses:
'200':
content:
application/json:
schema:
type: object
properties:
engines:
items:
$ref: '#/components/schemas/EngineDescriptor'
type: array
status:
$ref: '#/components/schemas/StoreStatus'
description: Successful response
summary: Get the status of the Entity Store
tags:
- Security Entity Analytics API
/api/risk_score/engine/dangerously_delete_data:
delete:
description: >-
@ -1046,6 +1102,14 @@ components:
- index
- description
- category
StoreStatus:
enum:
- not_installed
- installing
- running
- stopped
- error
type: string
TaskManagerUnavailableResponse:
description: Task manager is unavailable
type: object

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { EngineStatus } from '../../../../common/api/entity_analytics';
import type { EngineStatus, StoreStatus } from '../../../../common/api/entity_analytics';
export const DEFAULT_LOOKBACK_PERIOD = '24h';
@ -17,4 +17,12 @@ export const ENGINE_STATUS: Record<Uppercase<EngineStatus>, EngineStatus> = {
ERROR: 'error',
};
export const ENTITY_STORE_STATUS: Record<Uppercase<StoreStatus>, StoreStatus> = {
RUNNING: 'running',
STOPPED: 'stopped',
INSTALLING: 'installing',
NOT_INSTALLED: 'not_installed',
ERROR: 'error',
};
export const MAX_SEARCH_RESPONSE_SIZE = 10_000;

View file

@ -20,17 +20,22 @@ import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
import type { DataViewsService } from '@kbn/data-views-plugin/common';
import { isEqual } from 'lodash/fp';
import moment from 'moment';
import type {
GetEntityStoreStatusResponse,
InitEntityStoreRequestBody,
InitEntityStoreResponse,
} from '../../../../common/api/entity_analytics/entity_store/enablement.gen';
import type { AppClient } from '../../..';
import { EntityType } from '../../../../common/api/entity_analytics';
import type {
Entity,
EngineDataviewUpdateResult,
InitEntityEngineRequestBody,
InitEntityEngineResponse,
EntityType,
InspectQuery,
} from '../../../../common/api/entity_analytics';
import { EngineDescriptorClient } from './saved_object/engine_descriptor';
import { ENGINE_STATUS, MAX_SEARCH_RESPONSE_SIZE } from './constants';
import { ENGINE_STATUS, ENTITY_STORE_STATUS, MAX_SEARCH_RESPONSE_SIZE } from './constants';
import { AssetCriticalityEcsMigrationClient } from '../asset_criticality/asset_criticality_migration_client';
import { getUnitedEntityDefinition } from './united_entity_definitions';
import {
@ -126,6 +131,44 @@ export class EntityStoreDataClient {
});
}
public async enable(
{ indexPattern = '', filter = '', fieldHistoryLength = 10 }: InitEntityStoreRequestBody,
{ pipelineDebugMode = false }: { pipelineDebugMode?: boolean } = {}
): Promise<InitEntityStoreResponse> {
if (!this.options.taskManager) {
throw new Error('Task Manager is not available');
}
// Immediately defer the initialization to the next tick. This way we don't block on the init preflight checks
const run = <T>(fn: () => Promise<T>) =>
new Promise<T>((resolve) => setTimeout(() => fn().then(resolve), 0));
const promises = Object.values(EntityType.Values).map((entity) =>
run(() =>
this.init(entity, { indexPattern, filter, fieldHistoryLength }, { pipelineDebugMode })
)
);
const engines = await Promise.all(promises);
return { engines, succeeded: true };
}
public async status(): Promise<GetEntityStoreStatusResponse> {
const { engines, count } = await this.engineClient.list();
let status = ENTITY_STORE_STATUS.RUNNING;
if (count === 0) {
status = ENTITY_STORE_STATUS.NOT_INSTALLED;
} else if (engines.some((engine) => engine.status === ENGINE_STATUS.ERROR)) {
status = ENTITY_STORE_STATUS.ERROR;
} else if (engines.every((engine) => engine.status === ENGINE_STATUS.STOPPED)) {
status = ENTITY_STORE_STATUS.STOPPED;
} else if (engines.some((engine) => engine.status === ENGINE_STATUS.INSTALLING)) {
status = ENTITY_STORE_STATUS.INSTALLING;
}
return { engines, status };
}
public async init(
entityType: EntityType,
{ indexPattern = '', filter = '', fieldHistoryLength = 10 }: InitEntityEngineRequestBody,
@ -137,7 +180,16 @@ export class EntityStoreDataClient {
const { config } = this.options;
await this.riskScoreDataClient.createRiskScoreLatestIndex();
await this.riskScoreDataClient.createRiskScoreLatestIndex().catch((e) => {
if (e.meta.body.error.type === 'resource_already_exists_exception') {
this.options.logger.debug(
`Risk score index for ${entityType} already exists, skipping creation.`
);
return;
}
throw e;
});
const requiresMigration =
await this.assetCriticalityMigrationClient.isEcsDataMigrationRequired();

View file

@ -0,0 +1,67 @@
/*
* 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 type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
import { transformError } from '@kbn/securitysolution-es-utils';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import type { InitEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/enablement.gen';
import { InitEntityStoreRequestBody } from '../../../../../common/api/entity_analytics/entity_store/enablement.gen';
import { API_VERSIONS, APP_ID } from '../../../../../common/constants';
import type { EntityAnalyticsRoutesDeps } from '../../types';
import { checkAndInitAssetCriticalityResources } from '../../asset_criticality/check_and_init_asset_criticality_resources';
export const enableEntityStoreRoute = (
router: EntityAnalyticsRoutesDeps['router'],
logger: Logger,
config: EntityAnalyticsRoutesDeps['config']
) => {
router.versioned
.post({
access: 'public',
path: '/api/entity_store/enable',
security: {
authz: {
requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`],
},
},
})
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {
request: {
body: buildRouteValidationWithZod(InitEntityStoreRequestBody),
},
},
},
async (context, request, response): Promise<IKibanaResponse<InitEntityStoreResponse>> => {
const siemResponse = buildSiemResponse(response);
const secSol = await context.securitySolution;
const { pipelineDebugMode } = config.entityAnalytics.entityStore.developer;
await checkAndInitAssetCriticalityResources(context, logger);
try {
const body: InitEntityStoreResponse = await secSol
.getEntityStoreDataClient()
.enable(request.body, { pipelineDebugMode });
return response.ok({ body });
} catch (e) {
const error = transformError(e);
logger.error(`Error initialising entity store: ${error.message}`);
return siemResponse.error({
statusCode: error.statusCode,
body: error.message,
});
}
}
);
};

View file

@ -15,6 +15,8 @@ import { listEntityEnginesRoute } from './list';
import { entityStoreInternalPrivilegesRoute } from './privileges';
import { startEntityEngineRoute } from './start';
import { stopEntityEngineRoute } from './stop';
import { getEntityStoreStatusRoute } from './status';
import { enableEntityStoreRoute } from './enablement';
export const registerEntityStoreRoutes = ({
router,
@ -22,6 +24,8 @@ export const registerEntityStoreRoutes = ({
getStartServices,
config,
}: EntityAnalyticsRoutesDeps) => {
enableEntityStoreRoute(router, logger, config);
getEntityStoreStatusRoute(router, logger, config);
initEntityEngineRoute(router, logger, config);
startEntityEngineRoute(router, logger);
stopEntityEngineRoute(router, logger);

View file

@ -0,0 +1,70 @@
/*
* 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.
*/
/*
* 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 type { IKibanaResponse, Logger } from '@kbn/core/server';
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
import { transformError } from '@kbn/securitysolution-es-utils';
import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/enablement.gen';
import { API_VERSIONS, APP_ID } from '../../../../../common/constants';
import type { EntityAnalyticsRoutesDeps } from '../../types';
import { checkAndInitAssetCriticalityResources } from '../../asset_criticality/check_and_init_asset_criticality_resources';
export const getEntityStoreStatusRoute = (
router: EntityAnalyticsRoutesDeps['router'],
logger: Logger,
config: EntityAnalyticsRoutesDeps['config']
) => {
router.versioned
.get({
access: 'public',
path: '/api/entity_store/status',
security: {
authz: {
requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`],
},
},
})
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {},
},
async (
context,
request,
response
): Promise<IKibanaResponse<GetEntityStoreStatusResponse>> => {
const siemResponse = buildSiemResponse(response);
const secSol = await context.securitySolution;
await checkAndInitAssetCriticalityResources(context, logger);
try {
const body: GetEntityStoreStatusResponse = await secSol
.getEntityStoreDataClient()
.status();
return response.ok({ body });
} catch (e) {
const error = transformError(e);
logger.error(`Error initialising entity store: ${error.message}`);
return siemResponse.error({
statusCode: error.statusCode,
body: error.message,
});
}
}
);
};

View file

@ -106,6 +106,7 @@ import {
InitEntityEngineRequestParamsInput,
InitEntityEngineRequestBodyInput,
} from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/init.gen';
import { InitEntityStoreRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/enablement.gen';
import { InstallPrepackedTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route.gen';
import { ListEntitiesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/entities/list_entities.gen';
import { PatchRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/patch_rule/patch_rule_route.gen';
@ -842,6 +843,13 @@ finalize it.
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
getEntityStoreStatus(kibanaSpace: string = 'default') {
return supertest
.get(routeWithNamespace('/api/entity_store/status', kibanaSpace))
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
/**
* Get all notes for a given document.
*/
@ -1030,6 +1038,14 @@ finalize it.
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
initEntityStore(props: InitEntityStoreProps, kibanaSpace: string = 'default') {
return supertest
.post(routeWithNamespace('/api/entity_store/enable', kibanaSpace))
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
/**
* Initializes the Risk Engine by creating the necessary indices and mappings, removing old transforms, and starting the new risk engine
*/
@ -1633,6 +1649,9 @@ export interface InitEntityEngineProps {
params: InitEntityEngineRequestParamsInput;
body: InitEntityEngineRequestBodyInput;
}
export interface InitEntityStoreProps {
body: InitEntityStoreRequestBodyInput;
}
export interface InstallPrepackedTimelinesProps {
body: InstallPrepackedTimelinesRequestBodyInput;
}

View file

@ -14,7 +14,7 @@ export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const utils = EntityStoreUtils(getService);
describe('@ess @skipInServerlessMKI Entity Store Engine APIs', () => {
describe('@ess @skipInServerlessMKI Entity Store APIs', () => {
const dataView = dataViewRouteHelpersFactory(supertest);
before(async () => {
@ -42,6 +42,18 @@ export default ({ getService }: FtrProviderContext) => {
});
});
describe('enablement', () => {
afterEach(async () => {
await utils.cleanEngines();
});
it('should enable the entity store, creating both user and host engines', async () => {
await utils.enableEntityStore();
await utils.expectEngineAssetsExist('user');
await utils.expectEngineAssetsExist('host');
});
});
describe('get and list', () => {
before(async () => {
await utils.initEntityEngineForEntityTypesAndWait(['host', 'user']);
@ -182,6 +194,42 @@ export default ({ getService }: FtrProviderContext) => {
});
});
describe('status', () => {
afterEach(async () => {
await utils.cleanEngines();
});
it('should return "not_installed" when no engines have been initialized', async () => {
const { body } = await api.getEntityStoreStatus().expect(200);
expect(body).to.eql({
engines: [],
status: 'not_installed',
});
});
it('should return "installing" when at least one engine is being initialized', async () => {
await utils.enableEntityStore();
const { body } = await api.getEntityStoreStatus().expect(200);
expect(body.status).to.eql('installing');
expect(body.engines.length).to.eql(2);
expect(body.engines[0].status).to.eql('installing');
expect(body.engines[1].status).to.eql('installing');
});
it('should return "started" when all engines are started', async () => {
await utils.initEntityEngineForEntityTypesAndWait(['host', 'user']);
const { body } = await api.getEntityStoreStatus().expect(200);
expect(body.status).to.eql('running');
expect(body.engines.length).to.eql(2);
expect(body.engines[0].status).to.eql('started');
expect(body.engines[1].status).to.eql('started');
});
});
describe('apply_dataview_indices', () => {
before(async () => {
await utils.initEntityEngineForEntityTypesAndWait(['host']);

View file

@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Entity Analytics - Entity Store', function () {
loadTestFile(require.resolve('./entities_list'));
loadTestFile(require.resolve('./engine'));
loadTestFile(require.resolve('./entity_store'));
loadTestFile(require.resolve('./field_retention_operators'));
loadTestFile(require.resolve('./engine_nondefault_spaces'));
loadTestFile(require.resolve('./entity_store_nondefault_spaces'));
});
}

View file

@ -90,6 +90,16 @@ export const EntityStoreUtils = (
);
};
const enableEntityStore = async () => {
const res = await api.initEntityStore({ body: {} }, namespace);
if (res.status !== 200) {
log.error(`Failed to enable entity store`);
log.error(JSON.stringify(res.body));
}
expect(res.status).to.eql(200);
return res;
};
const expectTransformStatus = async (
transformId: string,
exists: boolean,
@ -144,5 +154,6 @@ export const EntityStoreUtils = (
expectTransformStatus,
expectEngineAssetsExist,
expectEngineAssetsDoNotExist,
enableEntityStore,
};
};