From 06b7993bd90cf84f87a99668c4425fb4dd6d6b5e Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Fri, 29 Nov 2024 16:29:04 +0100 Subject: [PATCH] [SecuritySolution] Entity Engine status tab (#201235) ## Summary * Add two tabs to the Entity Store page * The import entities tab has all the bulk upload content * The status tab has the new content created on this PR * Move the "clear entity store data" button to the header according to design mockups. * Delete unused stats route * Rename `enablement` API docs to `enable` * Add a new parameter to the status API (`withComponents`) * Should I make it snake cased? ### import entities tab ![Screenshot 2024-11-27 at 15 07 01](https://github.com/user-attachments/assets/c433e217-781e-4792-8695-2ee609efa654) ### status tab ![Screenshot 2024-11-27 at 15 07 20](https://github.com/user-attachments/assets/8970c023-22b3-4e83-a444-fa3ccf78ea42) ## How to test it - Open security solution app with data - Go to entity store page - You shouldn't see the new tab because the engine is disabled - Enable the engine and wait - Click on the new tab that showed up - It should list user and host engine components, and everything should be installed - Delete or misconfigure some of the resources, the new status should be reflected on the tab. ## TODO: - [x] Rebase main after https://github.com/elastic/kibana/pull/199762 is merged - [x] Remove temporary status hook - [x] Fix the"clear entity data" button. It should re-fetch the status API. ### Checklist Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- oas_docs/output/kibana.serverless.yaml | 95 ++++---- oas_docs/output/kibana.yaml | 94 +++++--- .../entity_store/common.gen.ts | 31 +++ .../entity_store/common.schema.yaml | 43 ++++ .../{enablement.gen.ts => enable.gen.ts} | 8 +- ...blement.schema.yaml => enable.schema.yaml} | 21 -- .../entity_store/engine/index.ts | 1 - .../entity_store/engine/stats.gen.ts | 39 ---- .../entity_store/engine/stats.schema.yaml | 41 ---- .../entity_store/status.gen.ts | 43 ++++ .../entity_store/status.schema.yaml | 44 ++++ .../common/api/quickstart_client.gen.ts | 31 +-- ...alytics_api_2023_10_31.bundled.schema.yaml | 96 +++++--- ...alytics_api_2023_10_31.bundled.schema.yaml | 96 +++++--- .../entity_analytics/api/entity_store.ts | 16 +- .../components/dashboard_enablement_panel.tsx | 2 +- .../engine_components_status.test.tsx | 118 ++++++++++ .../components/engine_components_status.tsx | 75 +++++++ .../engines_status/hooks/use_columns.tsx | 184 ++++++++++++++++ .../components/engines_status/index.test.tsx | 100 +++++++++ .../components/engines_status/index.tsx | 103 +++++++++ .../entity_store/hooks/use_entity_store.ts | 31 ++- .../entity_store_management_page.test.tsx | 207 ++++++++++++++++++ .../pages/entity_store_management_page.tsx | 156 +++++++------ .../entity_store/auditing/resources.ts | 18 -- .../component_template.ts | 25 +++ .../elasticsearch_assets/enrich_policy.ts | 19 ++ .../elasticsearch_assets/entity_index.ts | 24 +- .../elasticsearch_assets/ingest_pipeline.ts | 25 +++ .../entity_store/entity_store_data_client.ts | 166 ++++++++++++-- .../entity_store/routes/enablement.ts | 4 +- .../entity_store/routes/stats.ts | 64 ------ .../entity_store/routes/status.ts | 12 +- .../task/field_retention_enrichment_task.ts | 39 +++- .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../services/security_solution_api.gen.ts | 25 +-- .../entity_store.ts | 77 +++++-- 39 files changed, 1663 insertions(+), 516 deletions(-) rename x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/{enablement.gen.ts => enable.gen.ts} (78%) rename x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/{enablement.schema.yaml => enable.schema.yaml} (65%) delete mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.gen.ts delete mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.schema.yaml create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/status.gen.ts create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/status.schema.yaml create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_components_status.test.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_components_status.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/hooks/use_columns.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/index.tsx create mode 100644 x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.test.tsx delete mode 100644 x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/auditing/resources.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stats.ts diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 68a5ccaff2e10..959c4d886eee2 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -7560,42 +7560,6 @@ paths: tags: - Security Entity Analytics API x-beta: true - /api/entity_store/engines/{entityType}/stats: - post: - operationId: GetEntityEngineStats - parameters: - - description: The entity type of the engine (either 'user' or 'host'). - in: path - name: entityType - required: true - schema: - $ref: '#/components/schemas/Security_Entity_Analytics_API_EntityType' - responses: - '200': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - type: object - properties: - indexPattern: - $ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern' - indices: - items: - type: object - type: array - status: - $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineStatus' - transforms: - items: - type: object - type: array - type: - $ref: '#/components/schemas/Security_Entity_Analytics_API_EntityType' - description: Successful response - summary: Get Entity Engine stats - tags: - - Security Entity Analytics API - x-beta: true /api/entity_store/engines/{entityType}/stop: post: operationId: StopEntityEngine @@ -7749,6 +7713,12 @@ paths: /api/entity_store/status: get: operationId: GetEntityStoreStatus + parameters: + - description: If true returns a detailed status of the engine including all it's components + in: query + name: include_components + schema: + type: boolean responses: '200': content: @@ -7758,10 +7728,20 @@ paths: properties: engines: items: - $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor' + allOf: + - $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor' + - type: object + properties: + components: + items: + $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineComponentStatus' + type: array type: array status: $ref: '#/components/schemas/Security_Entity_Analytics_API_StoreStatus' + required: + - status + - engines description: Successful response summary: Get the status of the Entity Store tags: @@ -45755,6 +45735,47 @@ components: $ref: '#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel' required: - criticality_level + Security_Entity_Analytics_API_EngineComponentResource: + enum: + - entity_engine + - entity_definition + - index + - component_template + - index_template + - ingest_pipeline + - enrich_policy + - task + - transform + type: string + Security_Entity_Analytics_API_EngineComponentStatus: + type: object + properties: + errors: + items: + type: object + properties: + message: + type: string + title: + type: string + type: array + health: + enum: + - green + - yellow + - red + - unknown + type: string + id: + type: string + installed: + type: boolean + resource: + $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineComponentResource' + required: + - id + - installed + - resource Security_Entity_Analytics_API_EngineDataviewUpdateResult: type: object properties: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 208bced5d70f6..0b5b03378168e 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -10445,41 +10445,6 @@ paths: summary: Start an Entity Engine tags: - Security Entity Analytics API - /api/entity_store/engines/{entityType}/stats: - post: - operationId: GetEntityEngineStats - parameters: - - description: The entity type of the engine (either 'user' or 'host'). - in: path - name: entityType - required: true - schema: - $ref: '#/components/schemas/Security_Entity_Analytics_API_EntityType' - responses: - '200': - content: - application/json; Elastic-Api-Version=2023-10-31: - schema: - type: object - properties: - indexPattern: - $ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern' - indices: - items: - type: object - type: array - status: - $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineStatus' - transforms: - items: - type: object - type: array - type: - $ref: '#/components/schemas/Security_Entity_Analytics_API_EntityType' - description: Successful response - summary: Get Entity Engine stats - tags: - - Security Entity Analytics API /api/entity_store/engines/{entityType}/stop: post: operationId: StopEntityEngine @@ -10630,6 +10595,12 @@ paths: /api/entity_store/status: get: operationId: GetEntityStoreStatus + parameters: + - description: If true returns a detailed status of the engine including all it's components + in: query + name: include_components + schema: + type: boolean responses: '200': content: @@ -10639,10 +10610,20 @@ paths: properties: engines: items: - $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor' + allOf: + - $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor' + - type: object + properties: + components: + items: + $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineComponentStatus' + type: array type: array status: $ref: '#/components/schemas/Security_Entity_Analytics_API_StoreStatus' + required: + - status + - engines description: Successful response summary: Get the status of the Entity Store tags: @@ -53478,6 +53459,47 @@ components: $ref: '#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel' required: - criticality_level + Security_Entity_Analytics_API_EngineComponentResource: + enum: + - entity_engine + - entity_definition + - index + - component_template + - index_template + - ingest_pipeline + - enrich_policy + - task + - transform + type: string + Security_Entity_Analytics_API_EngineComponentStatus: + type: object + properties: + errors: + items: + type: object + properties: + message: + type: string + title: + type: string + type: array + health: + enum: + - green + - yellow + - red + - unknown + type: string + id: + type: string + installed: + type: boolean + resource: + $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineComponentResource' + required: + - id + - installed + - resource Security_Entity_Analytics_API_EngineDataviewUpdateResult: type: object properties: diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts index 7e419dbe6453c..25c47e838d85c 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts @@ -39,6 +39,37 @@ export const EngineDescriptor = z.object({ error: z.object({}).optional(), }); +export type EngineComponentResource = z.infer; +export const EngineComponentResource = z.enum([ + 'entity_engine', + 'entity_definition', + 'index', + 'component_template', + 'index_template', + 'ingest_pipeline', + 'enrich_policy', + 'task', + 'transform', +]); +export type EngineComponentResourceEnum = typeof EngineComponentResource.enum; +export const EngineComponentResourceEnum = EngineComponentResource.enum; + +export type EngineComponentStatus = z.infer; +export const EngineComponentStatus = z.object({ + id: z.string(), + installed: z.boolean(), + resource: EngineComponentResource, + health: z.enum(['green', 'yellow', 'red', 'unknown']).optional(), + errors: z + .array( + z.object({ + title: z.string().optional(), + message: z.string().optional(), + }) + ) + .optional(), +}); + export type StoreStatus = z.infer; export const StoreStatus = z.enum(['not_installed', 'installing', 'running', 'stopped', 'error']); export type StoreStatusEnum = typeof StoreStatus.enum; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml index 9a42191a556ac..5adb6fe038dc9 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml @@ -42,6 +42,49 @@ components: - updating - error + EngineComponentStatus: + type: object + required: + - id + - installed + - resource + properties: + id: + type: string + installed: + type: boolean + resource: + $ref: '#/components/schemas/EngineComponentResource' + health: + type: string + enum: + - green + - yellow + - red + - unknown + errors: + type: array + items: + type: object + properties: + title: + type: string + message: + type: string + + EngineComponentResource: + type: string + enum: + - entity_engine + - entity_definition + - index + - component_template + - index_template + - ingest_pipeline + - enrich_policy + - task + - transform + StoreStatus: type: string enum: diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enablement.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enable.gen.ts similarity index 78% rename from x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enablement.gen.ts rename to x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enable.gen.ts index 9644a1a333d16..70a58bf02be68 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enablement.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enable.gen.ts @@ -16,13 +16,7 @@ import { z } from '@kbn/zod'; -import { IndexPattern, EngineDescriptor, StoreStatus } from './common.gen'; - -export type GetEntityStoreStatusResponse = z.infer; -export const GetEntityStoreStatusResponse = z.object({ - status: StoreStatus.optional(), - engines: z.array(EngineDescriptor).optional(), -}); +import { IndexPattern, EngineDescriptor } from './common.gen'; export type InitEntityStoreRequestBody = z.infer; export const InitEntityStoreRequestBody = z.object({ diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enablement.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enable.schema.yaml similarity index 65% rename from x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enablement.schema.yaml rename to x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enable.schema.yaml index 306e876dfc4a7..81eec22d9ade9 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enablement.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/enable.schema.yaml @@ -41,24 +41,3 @@ paths: 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' diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/index.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/index.ts index b21308de36f18..32bc0a4efc07b 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/index.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/index.ts @@ -10,6 +10,5 @@ export * from './get.gen'; export * from './init.gen'; export * from './list.gen'; export * from './start.gen'; -export * from './stats.gen'; export * from './stop.gen'; export * from './apply_dataview_indices.gen'; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.gen.ts deleted file mode 100644 index 8b2cb44947535..0000000000000 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.gen.ts +++ /dev/null @@ -1,39 +0,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. - */ - -/* - * NOTICE: Do not edit this file manually. - * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. - * - * info: - * title: Get Entity Engine stats - * version: 2023-10-31 - */ - -import { z } from '@kbn/zod'; - -import { EntityType, IndexPattern, EngineStatus } from '../common.gen'; - -export type GetEntityEngineStatsRequestParams = z.infer; -export const GetEntityEngineStatsRequestParams = z.object({ - /** - * The entity type of the engine (either 'user' or 'host'). - */ - entityType: EntityType, -}); -export type GetEntityEngineStatsRequestParamsInput = z.input< - typeof GetEntityEngineStatsRequestParams ->; - -export type GetEntityEngineStatsResponse = z.infer; -export const GetEntityEngineStatsResponse = z.object({ - type: EntityType.optional(), - indexPattern: IndexPattern.optional(), - status: EngineStatus.optional(), - transforms: z.array(z.object({})).optional(), - indices: z.array(z.object({})).optional(), -}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.schema.yaml deleted file mode 100644 index 25c010acc92ce..0000000000000 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.schema.yaml +++ /dev/null @@ -1,41 +0,0 @@ -openapi: 3.0.0 - -info: - title: Get Entity Engine stats - version: '2023-10-31' -paths: - /api/entity_store/engines/{entityType}/stats: - post: - x-labels: [ess, serverless] - x-codegen-enabled: true - operationId: GetEntityEngineStats - summary: Get Entity Engine stats - parameters: - - name: entityType - in: path - required: true - schema: - $ref: '../common.schema.yaml#/components/schemas/EntityType' - description: The entity type of the engine (either 'user' or 'host'). - responses: - '200': - description: Successful response - content: - application/json: - schema: - type: object - properties: - type: - $ref : '../common.schema.yaml#/components/schemas/EntityType' - indexPattern: - $ref : '../common.schema.yaml#/components/schemas/IndexPattern' - status: - $ref : '../common.schema.yaml#/components/schemas/EngineStatus' - transforms: - type: array - items: - type: object - indices: - type: array - items: - type: object diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/status.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/status.gen.ts new file mode 100644 index 0000000000000..76249a787a43b --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/status.gen.ts @@ -0,0 +1,43 @@ +/* + * 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 { BooleanFromString } from '@kbn/zod-helpers'; + +import { StoreStatus, EngineDescriptor, EngineComponentStatus } from './common.gen'; + +export type GetEntityStoreStatusRequestQuery = z.infer; +export const GetEntityStoreStatusRequestQuery = z.object({ + /** + * If true returns a detailed status of the engine including all it's components + */ + include_components: BooleanFromString.optional(), +}); +export type GetEntityStoreStatusRequestQueryInput = z.input< + typeof GetEntityStoreStatusRequestQuery +>; + +export type GetEntityStoreStatusResponse = z.infer; +export const GetEntityStoreStatusResponse = z.object({ + status: StoreStatus, + engines: z.array( + EngineDescriptor.merge( + z.object({ + components: z.array(EngineComponentStatus).optional(), + }) + ) + ), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/status.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/status.schema.yaml new file mode 100644 index 0000000000000..a1be66282328e --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/status.schema.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.0 + +info: + title: Enable Entity Store + version: '2023-10-31' +paths: + /api/entity_store/status: + get: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: GetEntityStoreStatus + summary: Get the status of the Entity Store + + parameters: + - name: include_components + in: query + schema: + type: boolean + description: If true returns a detailed status of the engine including all it's components + + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + required: + - status + - engines + properties: + status: + $ref: './common.schema.yaml#/components/schemas/StoreStatus' + engines: + type: array + items: + allOf: + - $ref: './common.schema.yaml#/components/schemas/EngineDescriptor' + - type: object + properties: + components: + type: array + items: + $ref: './common.schema.yaml#/components/schemas/EngineComponentStatus' diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index 5a1cf49baecd1..016c77d7254dd 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -232,10 +232,9 @@ import type { UploadAssetCriticalityRecordsResponse, } from './entity_analytics/asset_criticality/upload_asset_criticality_csv.gen'; import type { - GetEntityStoreStatusResponse, InitEntityStoreRequestBodyInput, InitEntityStoreResponse, -} from './entity_analytics/entity_store/enablement.gen'; +} from './entity_analytics/entity_store/enable.gen'; import type { ApplyEntityEngineDataviewIndicesResponse } from './entity_analytics/entity_store/engine/apply_dataview_indices.gen'; import type { DeleteEntityEngineRequestQueryInput, @@ -257,10 +256,6 @@ import type { StartEntityEngineRequestParamsInput, StartEntityEngineResponse, } from './entity_analytics/entity_store/engine/start.gen'; -import type { - GetEntityEngineStatsRequestParamsInput, - GetEntityEngineStatsResponse, -} from './entity_analytics/entity_store/engine/stats.gen'; import type { StopEntityEngineRequestParamsInput, StopEntityEngineResponse, @@ -269,6 +264,10 @@ import type { ListEntitiesRequestQueryInput, ListEntitiesResponse, } from './entity_analytics/entity_store/entities/list_entities.gen'; +import type { + GetEntityStoreStatusRequestQueryInput, + GetEntityStoreStatusResponse, +} from './entity_analytics/entity_store/status.gen'; import type { CleanUpRiskEngineResponse } from './entity_analytics/risk_engine/engine_cleanup_route.gen'; import type { DisableRiskEngineResponse } from './entity_analytics/risk_engine/engine_disable_route.gen'; import type { EnableRiskEngineResponse } from './entity_analytics/risk_engine/engine_enable_route.gen'; @@ -1290,19 +1289,7 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } - async getEntityEngineStats(props: GetEntityEngineStatsProps) { - this.log.info(`${new Date().toISOString()} Calling API GetEntityEngineStats`); - return this.kbnClient - .request({ - path: replaceParams('/api/entity_store/engines/{entityType}/stats', props.params), - headers: { - [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', - }, - method: 'POST', - }) - .catch(catchAxiosErrorFormatAndThrow); - } - async getEntityStoreStatus() { + async getEntityStoreStatus(props: GetEntityStoreStatusProps) { this.log.info(`${new Date().toISOString()} Calling API GetEntityStoreStatus`); return this.kbnClient .request({ @@ -1311,6 +1298,8 @@ finalize it. [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', }, method: 'GET', + + query: props.query, }) .catch(catchAxiosErrorFormatAndThrow); } @@ -2285,8 +2274,8 @@ export interface GetEndpointSuggestionsProps { export interface GetEntityEngineProps { params: GetEntityEngineRequestParamsInput; } -export interface GetEntityEngineStatsProps { - params: GetEntityEngineStatsRequestParamsInput; +export interface GetEntityStoreStatusProps { + query: GetEntityStoreStatusRequestQueryInput; } export interface GetNotesProps { query: GetNotesRequestQueryInput; diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index fa79b170f3513..b1b85b8222786 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -430,41 +430,6 @@ paths: summary: Start an Entity Engine tags: - Security Entity Analytics API - /api/entity_store/engines/{entityType}/stats: - post: - operationId: GetEntityEngineStats - parameters: - - description: The entity type of the engine (either 'user' or 'host'). - in: path - name: entityType - required: true - schema: - $ref: '#/components/schemas/EntityType' - responses: - '200': - content: - application/json: - schema: - type: object - properties: - indexPattern: - $ref: '#/components/schemas/IndexPattern' - indices: - items: - type: object - type: array - status: - $ref: '#/components/schemas/EngineStatus' - transforms: - items: - type: object - type: array - type: - $ref: '#/components/schemas/EntityType' - description: Successful response - summary: Get Entity Engine stats - tags: - - Security Entity Analytics API /api/entity_store/engines/{entityType}/stop: post: operationId: StopEntityEngine @@ -615,6 +580,14 @@ paths: /api/entity_store/status: get: operationId: GetEntityStoreStatus + parameters: + - description: >- + If true returns a detailed status of the engine including all it's + components + in: query + name: include_components + schema: + type: boolean responses: '200': content: @@ -624,10 +597,20 @@ paths: properties: engines: items: - $ref: '#/components/schemas/EngineDescriptor' + allOf: + - $ref: '#/components/schemas/EngineDescriptor' + - type: object + properties: + components: + items: + $ref: '#/components/schemas/EngineComponentStatus' + type: array type: array status: $ref: '#/components/schemas/StoreStatus' + required: + - status + - engines description: Successful response summary: Get the status of the Entity Store tags: @@ -824,6 +807,47 @@ components: $ref: '#/components/schemas/AssetCriticalityLevel' required: - criticality_level + EngineComponentResource: + enum: + - entity_engine + - entity_definition + - index + - component_template + - index_template + - ingest_pipeline + - enrich_policy + - task + - transform + type: string + EngineComponentStatus: + type: object + properties: + errors: + items: + type: object + properties: + message: + type: string + title: + type: string + type: array + health: + enum: + - green + - yellow + - red + - unknown + type: string + id: + type: string + installed: + type: boolean + resource: + $ref: '#/components/schemas/EngineComponentResource' + required: + - id + - installed + - resource EngineDataviewUpdateResult: type: object properties: diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 9c2b3d62b1650..4a3b3495467e9 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -430,41 +430,6 @@ paths: summary: Start an Entity Engine tags: - Security Entity Analytics API - /api/entity_store/engines/{entityType}/stats: - post: - operationId: GetEntityEngineStats - parameters: - - description: The entity type of the engine (either 'user' or 'host'). - in: path - name: entityType - required: true - schema: - $ref: '#/components/schemas/EntityType' - responses: - '200': - content: - application/json: - schema: - type: object - properties: - indexPattern: - $ref: '#/components/schemas/IndexPattern' - indices: - items: - type: object - type: array - status: - $ref: '#/components/schemas/EngineStatus' - transforms: - items: - type: object - type: array - type: - $ref: '#/components/schemas/EntityType' - description: Successful response - summary: Get Entity Engine stats - tags: - - Security Entity Analytics API /api/entity_store/engines/{entityType}/stop: post: operationId: StopEntityEngine @@ -615,6 +580,14 @@ paths: /api/entity_store/status: get: operationId: GetEntityStoreStatus + parameters: + - description: >- + If true returns a detailed status of the engine including all it's + components + in: query + name: include_components + schema: + type: boolean responses: '200': content: @@ -624,10 +597,20 @@ paths: properties: engines: items: - $ref: '#/components/schemas/EngineDescriptor' + allOf: + - $ref: '#/components/schemas/EngineDescriptor' + - type: object + properties: + components: + items: + $ref: '#/components/schemas/EngineComponentStatus' + type: array type: array status: $ref: '#/components/schemas/StoreStatus' + required: + - status + - engines description: Successful response summary: Get the status of the Entity Store tags: @@ -824,6 +807,47 @@ components: $ref: '#/components/schemas/AssetCriticalityLevel' required: - criticality_level + EngineComponentResource: + enum: + - entity_engine + - entity_definition + - index + - component_template + - index_template + - ingest_pipeline + - enrich_policy + - task + - transform + type: string + EngineComponentStatus: + type: object + properties: + errors: + items: + type: object + properties: + message: + type: string + title: + type: string + type: array + health: + enum: + - green + - yellow + - red + - unknown + type: string + id: + type: string + installed: + type: boolean + resource: + $ref: '#/components/schemas/EngineComponentResource' + required: + - id + - installed + - resource EngineDataviewUpdateResult: type: object properties: diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts index f1afa13637bb8..0098b842748e2 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/entity_store.ts @@ -5,15 +5,14 @@ * 2.0. */ import { useMemo } from 'react'; +import type { GetEntityStoreStatusResponse } from '../../../common/api/entity_analytics/entity_store/status.gen'; import type { - GetEntityStoreStatusResponse, InitEntityStoreRequestBody, InitEntityStoreResponse, -} from '../../../common/api/entity_analytics/entity_store/enablement.gen'; +} from '../../../common/api/entity_analytics/entity_store/enable.gen'; import type { DeleteEntityEngineResponse, EntityType, - GetEntityEngineResponse, InitEntityEngineResponse, ListEntityEnginesResponse, StopEntityEngineResponse, @@ -35,10 +34,11 @@ export const useEntityStoreRoutes = () => { }); }; - const getEntityStoreStatus = async () => { + const getEntityStoreStatus = async (withComponents = false) => { return http.fetch('/api/entity_store/status', { method: 'GET', version: API_VERSIONS.public.v1, + query: { include_components: withComponents }, }); }; @@ -58,13 +58,6 @@ export const useEntityStoreRoutes = () => { }); }; - const getEntityEngine = async (entityType: EntityType) => { - return http.fetch(`/api/entity_store/engines/${entityType}`, { - method: 'GET', - version: API_VERSIONS.public.v1, - }); - }; - const deleteEntityEngine = async (entityType: EntityType, deleteData: boolean) => { return http.fetch(`/api/entity_store/engines/${entityType}`, { method: 'DELETE', @@ -85,7 +78,6 @@ export const useEntityStoreRoutes = () => { getEntityStoreStatus, initEntityEngine, stopEntityEngine, - getEntityEngine, deleteEntityEngine, listEntityEngines, }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx index 8d0426fd99ceb..38449fa1e72ff 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_enablement_panel.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import type { UseQueryResult } from '@tanstack/react-query'; -import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/enablement.gen'; +import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/status.gen'; import type { StoreStatus } from '../../../../../common/api/entity_analytics'; import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics'; import { useInitRiskEngineMutation } from '../../../api/hooks/use_init_risk_engine_mutation'; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_components_status.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_components_status.test.tsx new file mode 100644 index 0000000000000..e97c9f961bb50 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_components_status.test.tsx @@ -0,0 +1,118 @@ +/* + * 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 React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { EngineComponentsStatusTable } from './engine_components_status'; +import { + EngineComponentResourceEnum, + type EngineComponentStatus, +} from '../../../../../../../common/api/entity_analytics'; +import { TestProviders } from '../../../../../../common/mock'; + +const uninstalledWithErrorsComponent = { + id: 'entity_engine_id', + installed: false, + resource: EngineComponentResourceEnum.entity_engine, + errors: [{ title: 'Error 1', message: 'Error message 1' }], +}; + +const installedComponent = { + id: 'index_id', + resource: EngineComponentResourceEnum.index, + errors: [], + installed: true, +}; + +const mockComponents: EngineComponentStatus[] = [ + uninstalledWithErrorsComponent, + installedComponent, +]; + +const mockGetUrlForApp = jest.fn(); + +jest.mock('../../../../../../common/lib/kibana', () => { + return { + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + getUrlForApp: () => mockGetUrlForApp(), + navigateToApp: jest.fn(), + }, + }, + }), + }; +}); + +describe('EngineComponentsStatusTable', () => { + it('renders the table with components', () => { + render(, { wrapper: TestProviders }); + expect(screen.getByTestId('engine-components-status-table')).toBeInTheDocument(); + }); + + it('expands and collapses rows correctly', () => { + render(, { wrapper: TestProviders }); + + const toggleButton = screen.getByLabelText('Expand'); + fireEvent.click(toggleButton); + + expect(screen.getByText('Error 1')).toBeInTheDocument(); + expect(screen.getByText('Error message 1')).toBeInTheDocument(); + + fireEvent.click(toggleButton); + + expect(screen.queryByText('Error 1')).not.toBeInTheDocument(); + expect(screen.queryByText('Error message 1')).not.toBeInTheDocument(); + }); + + describe('columns', () => { + it('renders the correct resource text', () => { + render(, { + wrapper: TestProviders, + }); + expect(screen.getByText('Index')).toBeInTheDocument(); + }); + + it('renders checkmark on installation column when installed', () => { + render(, { + wrapper: TestProviders, + }); + + const icon = screen.getByTestId('installation-status'); + + expect(icon).toHaveAttribute('data-euiicon-type', 'check'); + }); + + it('renders cross on installation column when installed', () => { + render(, { + wrapper: TestProviders, + }); + + const icon = screen.getByTestId('installation-status'); + + expect(icon).toHaveAttribute('data-euiicon-type', 'cross'); + }); + + it('renders the correct health status', () => { + render(, { + wrapper: TestProviders, + }); + + expect(screen.queryByRole('img', { name: /health/i })).not.toBeInTheDocument(); + }); + + it('renders the correct identifier link', () => { + mockGetUrlForApp.mockReturnValue('mockedUrl'); + + render(, { + wrapper: TestProviders, + }); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', 'mockedUrl'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_components_status.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_components_status.tsx new file mode 100644 index 0000000000000..eb789035d566f --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_components_status.tsx @@ -0,0 +1,75 @@ +/* + * 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 { ReactNode } from 'react'; +import React, { useState, useMemo, useCallback, Fragment } from 'react'; +import { EuiSpacer, EuiHealth, EuiCodeBlock } from '@elastic/eui'; +import { BasicTable } from '../../../../../../common/components/ml/tables/basic_table'; +import { useColumns } from '../hooks/use_columns'; +import type { EngineComponentStatus } from '../../../../../../../common/api/entity_analytics'; + +type ExpandedRowMap = Record; + +const componentToId = ({ id, resource }: EngineComponentStatus) => `${resource}-${id}`; + +export const EngineComponentsStatusTable = ({ + components, +}: { + components: EngineComponentStatus[]; +}) => { + const [expandedItems, setExpandedItems] = useState([]); + + const itemIdToExpandedRowMap: ExpandedRowMap = useMemo(() => { + return expandedItems.reduce((acc, componentStatus) => { + if (componentStatus.errors && componentStatus.errors.length > 0) { + acc[componentToId(componentStatus)] = ( + + ); + } + return acc; + }, {}); + }, [expandedItems]); + + const onToggle = useCallback( + (component: EngineComponentStatus) => { + const isItemExpanded = expandedItems.includes(component); + + if (isItemExpanded) { + setExpandedItems(expandedItems.filter((item) => component !== item)); + } else { + setExpandedItems([...expandedItems, component]); + } + }, + [expandedItems] + ); + + const columns = useColumns(onToggle, expandedItems); + + return ( + + ); +}; + +const TransformExtendedData = ({ errors }: { errors: EngineComponentStatus['errors'] }) => { + return ( + <> + {errors?.map(({ title, message }) => ( + + + {title} + + {message} + + ))} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/hooks/use_columns.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/hooks/use_columns.tsx new file mode 100644 index 0000000000000..3156c768bf4fc --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/hooks/use_columns.tsx @@ -0,0 +1,184 @@ +/* + * 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 { IconColor } from '@elastic/eui'; +import { + EuiLink, + type EuiBasicTableColumn, + EuiHealth, + EuiScreenReaderOnly, + EuiButtonIcon, + EuiIcon, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EngineComponentResourceEnum, + type EngineComponentResource, +} from '../../../../../../../common/api/entity_analytics'; +import type { EngineComponentStatus } from '../../../../../../../common/api/entity_analytics'; +import { useKibana } from '../../../../../../common/lib/kibana'; + +type TableColumn = EuiBasicTableColumn; + +export const HEALTH_COLOR: Record['health'], IconColor> = { + green: 'success', + unknown: 'subdued', + yellow: 'warning', + red: 'danger', +} as const; + +const RESOURCE_TO_TEXT: Record = { + ingest_pipeline: 'Ingest Pipeline', + enrich_policy: 'Enrich Policy', + index: 'Index', + component_template: 'Component Template', + task: 'Task', + transform: 'Transform', + entity_definition: 'Entity Definition', + entity_engine: 'Engine', + index_template: 'Index Template', +}; + +export const useColumns = ( + onToggleExpandedItem: (item: EngineComponentStatus) => void, + expandedItems: EngineComponentStatus[] +): TableColumn[] => { + const { getUrlForApp } = useKibana().services.application; + + return useMemo( + () => [ + { + field: 'resource', + name: ( + + ), + width: '20%', + render: (resource: EngineComponentStatus['resource']) => RESOURCE_TO_TEXT[resource], + }, + { + field: 'id', + name: ( + + ), + render: (id: EngineComponentStatus['id'], { resource, installed }) => { + const path = getResourcePath(id, resource); + + if (!installed || !path) { + return id; + } + + return ( + + {id} + + ); + }, + }, + { + field: 'installed', + name: ( + + ), + width: '10%', + align: 'center', + render: (value: boolean) => + value ? ( + + ) : ( + + ), + }, + { + name: ( + + ), + width: '10%', + align: 'center', + render: ({ installed, resource, health }: EngineComponentStatus) => { + if (!installed) { + return null; + } + + return ; + }, + }, + { + isExpander: true, + align: 'right', + width: '40px', + name: ( + + + + + + ), + mobileOptions: { header: false }, + render: (component: EngineComponentStatus) => { + const isItemExpanded = expandedItems.includes(component); + + return component.errors && component.errors.length > 0 ? ( + onToggleExpandedItem(component)} + aria-label={isItemExpanded ? 'Collapse' : 'Expand'} + iconType={isItemExpanded ? 'arrowDown' : 'arrowRight'} + /> + ) : null; + }, + }, + ], + [expandedItems, getUrlForApp, onToggleExpandedItem] + ); +}; + +const getResourcePath = (id: string, resource: EngineComponentResource) => { + if (resource === EngineComponentResourceEnum.ingest_pipeline) { + return `ingest/ingest_pipelines?pipeline=${id}`; + } + + if (resource === EngineComponentResourceEnum.index_template) { + return `data/index_management/templates/${id}`; + } + + if (resource === EngineComponentResourceEnum.index) { + return `data/index_management/indices/index_details?indexName=${id}`; + } + + if (resource === EngineComponentResourceEnum.component_template) { + return `data/index_management/component_templates/${id}`; + } + + if (resource === EngineComponentResourceEnum.enrich_policy) { + return `data/index_management/enrich_policies?policy=${id}`; + } + + if (resource === EngineComponentResourceEnum.transform) { + return `data/transform/enrich_policies?_a=(transform:(queryText:'${id}'))`; + } + return null; +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/index.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/index.test.tsx new file mode 100644 index 0000000000000..2f9e74df39d39 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/index.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { EngineStatus } from '.'; + +import { TestProviders } from '@kbn/timelines-plugin/public/mock'; + +const mockUseEntityStore = jest.fn(); +jest.mock('../../hooks/use_entity_store', () => ({ + useEntityStoreStatus: () => mockUseEntityStore(), +})); + +const mockDownloadBlob = jest.fn(); +jest.mock('../../../../../common/utils/download_blob', () => ({ + downloadBlob: () => mockDownloadBlob(), +})); + +describe('EngineStatus', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders loading spinner when data is loading', () => { + mockUseEntityStore.mockReturnValue({ + data: undefined, + isLoading: true, + error: null, + }); + + render(, { wrapper: TestProviders }); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); + + it('renders error state when there is an error', () => { + mockUseEntityStore.mockReturnValue({ + data: null, + isLoading: false, + error: new Error('Error'), + }); + + render(, { wrapper: TestProviders }); + + expect(screen.getByText('There was an error loading the engine status')).toBeInTheDocument(); + }); + + it('renders "No engines found" message when there are no engines', () => { + mockUseEntityStore.mockReturnValue({ + data: { engines: [] }, + isLoading: false, + error: null, + }); + + render(, { wrapper: TestProviders }); + + expect(screen.getByText('No engines found')).toBeInTheDocument(); + }); + + it('renders engine components when data is available', () => { + const mockData = { + engines: [ + { + type: 'test', + components: [{ id: 'entity_engine_id', installed: true, resource: 'entity_engine' }], + }, + ], + }; + mockUseEntityStore.mockReturnValue({ data: mockData, isLoading: false, error: null }); + + render(, { wrapper: TestProviders }); + + expect(screen.getByText('Test Store')).toBeInTheDocument(); + expect(screen.getByText('Download status')).toBeInTheDocument(); + }); + + it('calls downloadJson when download button is clicked', () => { + const mockData = { + engines: [ + { + type: 'test', + components: [{ id: 'entity_engine_id', installed: true, resource: 'entity_engine' }], + }, + ], + }; + mockUseEntityStore.mockReturnValue({ data: mockData, isLoading: false, error: null }); + + render(, { wrapper: TestProviders }); + + const downloadButton = screen.getByText('Download status'); + fireEvent.click(downloadButton); + + expect(mockDownloadBlob).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/index.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/index.tsx new file mode 100644 index 0000000000000..692841334bd50 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/index.tsx @@ -0,0 +1,103 @@ +/* + * 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 React, { Fragment } from 'react'; +import { + EuiLoadingSpinner, + EuiPanel, + EuiSpacer, + EuiButtonEmpty, + EuiFlexItem, + EuiTitle, + EuiFlexGroup, + EuiCallOut, +} from '@elastic/eui'; +import { capitalize } from 'lodash/fp'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { useErrorToast } from '../../../../../common/hooks/use_error_toast'; +import { downloadBlob } from '../../../../../common/utils/download_blob'; +import { EngineComponentsStatusTable } from './components/engine_components_status'; +import { useEntityStoreStatus } from '../../hooks/use_entity_store'; + +const FILE_NAME = 'engines_status.json'; + +export const EngineStatus: React.FC = () => { + const { data, isLoading, error } = useEntityStoreStatus({ withComponents: true }); + + const downloadJson = () => { + downloadBlob(new Blob([JSON.stringify(data)]), FILE_NAME); + }; + + const errorMessage = i18n.translate( + 'xpack.securitySolution.entityAnalytics.entityStore.enginesStatus.queryError', + { + defaultMessage: 'There was an error loading the engine status', + } + ); + + useErrorToast(errorMessage, error); + + if (error) { + return ; + } + + if (!data || isLoading) return ; + + if (data.engines.length === 0) { + return ( + + ); + } + + return ( + + {data?.engines?.length > 0 && ( + + + + + + + + + + )} + + {(data?.engines ?? []).map((engine) => ( + + +

+ +

+
+ + + + + {engine.components && } + + + +
+ ))} +
+
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts index b27b5b4cdf26a..ceb93d164af62 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_store.ts @@ -5,13 +5,12 @@ * 2.0. */ -import type { UseMutationOptions, UseQueryOptions } from '@tanstack/react-query'; +import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import type { - GetEntityStoreStatusResponse, - InitEntityStoreResponse, -} from '../../../../../common/api/entity_analytics/entity_store/enablement.gen'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/status.gen'; +import type { InitEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/enable.gen'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; import type { DeleteEntityEngineResponse, @@ -26,19 +25,24 @@ const ENTITY_STORE_STATUS = ['GET', 'ENTITY_STORE_STATUS']; interface ResponseError { body: { message: string }; } -export const useEntityStoreStatus = (options: UseQueryOptions) => { + +interface Options { + withComponents?: boolean; +} + +export const useEntityStoreStatus = (opts: Options = {}) => { const { getEntityStoreStatus } = useEntityStoreRoutes(); - const query = useQuery(ENTITY_STORE_STATUS, getEntityStoreStatus, { + return useQuery({ + queryKey: [...ENTITY_STORE_STATUS, opts.withComponents], + queryFn: () => getEntityStoreStatus(opts.withComponents), refetchInterval: (data) => { if (data?.status === 'installing') { return 5000; } return false; }, - ...options, }); - return query; }; export const ENABLE_STORE_STATUS_KEY = ['POST', 'ENABLE_ENTITY_STORE']; @@ -102,15 +106,18 @@ export const useStopEntityEngineMutation = (options?: UseMutationOptions<{}>) => }; export const DELETE_ENTITY_ENGINE_STATUS_KEY = ['POST', 'STOP_ENTITY_ENGINE']; -export const useDeleteEntityEngineMutation = (options?: UseMutationOptions<{}>) => { +export const useDeleteEntityEngineMutation = ({ onSuccess }: { onSuccess?: () => void }) => { const queryClient = useQueryClient(); const { deleteEntityEngine } = useEntityStoreRoutes(); + return useMutation( () => Promise.all([deleteEntityEngine('user', true), deleteEntityEngine('host', true)]), { mutationKey: DELETE_ENTITY_ENGINE_STATUS_KEY, - onSuccess: () => queryClient.refetchQueries({ queryKey: ENTITY_STORE_STATUS }), - ...options, + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ENTITY_STORE_STATUS }); + onSuccess?.(); + }, } ); }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.test.tsx new file mode 100644 index 0000000000000..cc15a5360a3d1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.test.tsx @@ -0,0 +1,207 @@ +/* + * 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 React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { EntityStoreManagementPage } from './entity_store_management_page'; +import { TestProviders } from '../../common/mock'; + +jest.mock('../components/entity_store/components/engines_status', () => ({ + EngineStatus: () => {'Mocked Engine Status Tab'}, +})); + +const mockUseAssetCriticalityPrivileges = jest.fn(); +jest.mock('../components/asset_criticality/use_asset_criticality', () => ({ + useAssetCriticalityPrivileges: () => mockUseAssetCriticalityPrivileges(), +})); + +const mockUseIsExperimentalFeatureEnabled = jest.fn(); +jest.mock('../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: () => mockUseIsExperimentalFeatureEnabled(), +})); + +const mockUseHasSecurityCapability = jest.fn().mockReturnValue(true); +jest.mock('../../helper_hooks', () => ({ + useHasSecurityCapability: () => mockUseHasSecurityCapability(), +})); + +const mockUseEntityStoreStatus = jest.fn(); +jest.mock('../components/entity_store/hooks/use_entity_store', () => ({ + ...jest.requireActual('../components/entity_store/hooks/use_entity_store'), + useEntityStoreStatus: () => mockUseEntityStoreStatus(), +})); + +const mockUseEntityEnginePrivileges = jest.fn(); +jest.mock('../components/entity_store/hooks/use_entity_engine_privileges', () => ({ + useEntityEnginePrivileges: () => mockUseEntityEnginePrivileges(), +})); + +describe('EntityStoreManagementPage', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseAssetCriticalityPrivileges.mockReturnValue({ + isLoading: false, + data: { has_write_permissions: true }, + }); + + mockUseEntityEnginePrivileges.mockReturnValue({ + data: { has_all_required: true }, + }); + + mockUseEntityStoreStatus.mockReturnValue({ + data: { + status: 'running', + }, + errors: [], + }); + + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + }); + + it('does not render page when asset criticality is loading', () => { + mockUseAssetCriticalityPrivileges.mockReturnValue({ + isLoading: true, + }); + + render(, { wrapper: TestProviders }); + + expect(screen.queryByTestId('entityStoreManagementPage')).not.toBeInTheDocument(); + }); + + it('renders the page header', () => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + + render(, { wrapper: TestProviders }); + + expect(screen.getByTestId('entityStoreManagementPage')).toBeInTheDocument(); + expect(screen.getByText('Entity Store')).toBeInTheDocument(); + }); + + it('disables the switch when status is installing', () => { + mockUseEntityStoreStatus.mockReturnValue({ + data: { + status: 'installing', + }, + errors: [], + }); + + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + + render(, { wrapper: TestProviders }); + + expect(screen.getByTestId('entity-store-switch')).toBeDisabled(); + }); + + it('show clear entity data modal when clear data button clicked', () => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + + render(, { wrapper: TestProviders }); + + fireEvent.click(screen.getByText('Clear Entity Data')); + + expect(screen.getByText('Clear Entity data?')).toBeInTheDocument(); + }); + + it('renders the AssetCriticalityIssueCallout when there is an error', () => { + mockUseAssetCriticalityPrivileges.mockReturnValue({ + isLoading: false, + error: { body: { message: 'Error message', status_code: 403 } }, + }); + + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + + render(, { wrapper: TestProviders }); + + expect( + screen.getByText('Asset criticality CSV file upload functionality unavailable.') + ).toBeInTheDocument(); + expect(screen.getByText('Error message')).toBeInTheDocument(); + }); + + it('renders the InsufficientAssetCriticalityPrivilegesCallout when there are no write permissions', () => { + mockUseAssetCriticalityPrivileges.mockReturnValue({ + isLoading: false, + data: { has_write_permissions: false }, + }); + + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + + render(, { wrapper: TestProviders }); + + expect( + screen.getByText('Insufficient index privileges to perform CSV upload') + ).toBeInTheDocument(); + }); + + it('selects the Import tab by default', () => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + + render(, { wrapper: TestProviders }); + + expect(screen.getByText('Import Entities').parentNode).toHaveAttribute('aria-selected', 'true'); + }); + + it('switches to the Status tab when clicked', () => { + mockUseEntityStoreStatus.mockReturnValue({ + data: { + status: 'running', + }, + errors: [], + }); + + mockUseEntityEnginePrivileges.mockReturnValue({ + data: { has_all_required: true }, + }); + + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + + render(, { wrapper: TestProviders }); + + fireEvent.click(screen.getByText('Engine Status')); + + expect(screen.getByText('Engine Status').parentNode).toHaveAttribute('aria-selected', 'true'); + }); + + it('does not render the Status tab when entity store is not installed', () => { + mockUseEntityStoreStatus.mockReturnValue({ + data: { + status: 'not_installed', + }, + errors: [], + }); + + mockUseEntityEnginePrivileges.mockReturnValue({ + data: { has_all_required: true }, + }); + + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + + render(, { wrapper: TestProviders }); + + expect(screen.queryByText('Engine Status')).not.toBeInTheDocument(); + }); + + it('does not render the Status tab when privileges are missing', () => { + mockUseEntityStoreStatus.mockReturnValue({ + data: { + status: 'running', + }, + errors: [], + }); + + mockUseEntityEnginePrivileges.mockReturnValue({ + data: { has_all_required: false, privileges: { kibana: {}, elasticsearch: {} } }, + }); + + mockUseIsExperimentalFeatureEnabled.mockReturnValue(false); + + render(, { wrapper: TestProviders }); + + expect(screen.queryByText('Engine Status')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx index 7c00f61f62fbb..c0b9d8d697e53 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_store_management_page.tsx @@ -21,13 +21,15 @@ import { EuiCode, EuiSwitch, EuiHealth, - EuiButton, EuiLoadingSpinner, EuiToolTip, EuiBetaBadge, + EuiTabs, + EuiTab, + EuiButtonEmpty, } from '@elastic/eui'; import type { ReactNode } from 'react'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import type { SecurityAppError } from '@kbn/securitysolution-t-grid'; @@ -47,11 +49,18 @@ import { import { TECHNICAL_PREVIEW, TECHNICAL_PREVIEW_TOOLTIP } from '../../common/translations'; import { useEntityEnginePrivileges } from '../components/entity_store/hooks/use_entity_engine_privileges'; import { MissingPrivilegesCallout } from '../components/entity_store/components/missing_privileges_callout'; +import { EngineStatus } from '../components/entity_store/components/engines_status'; + +enum TabId { + Import = 'import', + Status = 'status', +} const isSwitchDisabled = (status?: StoreStatus) => status === 'error' || status === 'installing'; const isEntityStoreEnabled = (status?: StoreStatus) => status === 'running'; const canDeleteEntityEngine = (status?: StoreStatus) => !['not_installed', 'installing'].includes(status || ''); +const isEntityStoreInstalled = (status?: StoreStatus) => status && status !== 'not_installed'; export const EntityStoreManagementPage = () => { const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics'); @@ -62,7 +71,7 @@ export const EntityStoreManagementPage = () => { isLoading: assetCriticalityIsLoading, } = useAssetCriticalityPrivileges('AssetCriticalityUploadPage'); const hasAssetCriticalityWritePermissions = assetCriticalityPrivileges?.has_write_permissions; - + const [selectedTabId, setSelectedTabId] = useState(TabId.Import); const entityStoreStatus = useEntityStoreStatus({}); const enableStoreMutation = useEnableEntityStoreMutation(); @@ -91,6 +100,15 @@ export const EntityStoreManagementPage = () => { const { data: privileges } = useEntityEnginePrivileges(); + const shouldDisplayEngineStatusTab = + isEntityStoreInstalled(entityStoreStatus.data?.status) && privileges?.has_all_required; + + useEffect(() => { + if (selectedTabId === TabId.Status && !shouldDisplayEngineStatusTab) { + setSelectedTabId(TabId.Import); + } + }, [shouldDisplayEngineStatusTab, selectedTabId]); + if (assetCriticalityIsLoading) { // Wait for permission before rendering content to avoid flickering return null; @@ -149,8 +167,18 @@ export const EntityStoreManagementPage = () => { onSwitch={onSwitchClick} status={entityStoreStatus.data?.status} />, + canDeleteEntityEngine(entityStoreStatus.data?.status) ? ( + + ) : null, ] - : [] + : undefined } /> @@ -169,14 +197,44 @@ export const EntityStoreManagementPage = () => { )} - - + + + + setSelectedTabId(TabId.Import)} + > + + + + {shouldDisplayEngineStatusTab && ( + setSelectedTabId(TabId.Status)} + > + + + )} + + + - + {selectedTabId === TabId.Import && ( + + )} + {selectedTabId === TabId.Status && } {enableStoreMutation.isError && ( @@ -210,19 +268,7 @@ export const EntityStoreManagementPage = () => {
)} {callouts} - - {!isEntityStoreFeatureFlagDisabled && - privileges?.has_all_required && - canDeleteEntityEngine(entityStoreStatus.data?.status) && ( - - )} + {selectedTabId === TabId.Import && } @@ -375,10 +421,8 @@ const InsufficientAssetCriticalityPrivilegesCallout: React.FC = () => { ); }; -const AssetCriticalityIssueCallout: React.FC = ({ +const AssetCriticalityIssueCallout: React.FC<{ errorMessage?: string | ReactNode }> = ({ errorMessage, -}: { - errorMessage?: string | ReactNode; }) => { const msg = errorMessage ?? ( ; isClearModalVisible: boolean; closeClearModal: () => void; @@ -413,37 +457,19 @@ const ClearEntityDataPanel: React.FC<{ }> = ({ deleteEntityEngineMutation, isClearModalVisible, closeClearModal, showClearModal }) => { return ( <> - - -

- -

- - -
- - { - showClearModal(); - }} - > - - -
+ { + showClearModal(); + }} + > + + + {isClearModalVisible && ( { if (!hasEntityAnalyticsCapability || assetCriticalityPrivilegesError?.body.status_code === 403) { - return ; + return ( + + ); } if (!hasAssetCriticalityWritePermissions) { return ; } return ( - -

- -

-
`${definitionId}-latest@platform`; @@ -41,3 +45,24 @@ export const deleteEntityIndexComponentTemplate = ({ unitedDefinition, esClient } ); }; + +export const getEntityIndexComponentTemplateStatus = async ({ + definitionId, + esClient, +}: Pick & { definitionId: string }): Promise => { + const name = getComponentTemplateName(definitionId); + const componentTemplate = await esClient.cluster.getComponentTemplate( + { + name, + }, + { + ignore: [404], + } + ); + + return { + id: name, + installed: componentTemplate?.component_templates?.length > 0, + resource: EngineComponentResourceEnum.component_template, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/enrich_policy.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/enrich_policy.ts index 4b8ce594a6cb7..7064a4dd98851 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/enrich_policy.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/enrich_policy.ts @@ -7,6 +7,7 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import type { EnrichPutPolicyRequest } from '@elastic/elasticsearch/lib/api/types'; +import { EngineComponentResourceEnum } from '../../../../../common/api/entity_analytics'; import { getEntitiesIndexName } from '../utils'; import type { UnitedEntityDefinition } from '../united_entity_definitions'; @@ -105,3 +106,21 @@ export const deleteFieldRetentionEnrichPolicy = async ({ } } }; + +export const getFieldRetentionEnrichPolicyStatus = async ({ + definitionMetadata, + esClient, +}: { + definitionMetadata: DefinitionMetadata; + esClient: ElasticsearchClient; +}) => { + const name = getFieldRetentionEnrichPolicyName(definitionMetadata); + const policy = await esClient.enrich.getPolicy({ name }, { ignore: [404] }); + const policies = policy.policies; + + return { + installed: policies.length > 0, + id: name, + resource: EngineComponentResourceEnum.enrich_policy, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts index 6fb5935618dfc..0de8fe20050ce 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts @@ -6,7 +6,11 @@ */ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; -import type { EntityType } from '../../../../../common/api/entity_analytics'; +import { + EngineComponentResourceEnum, + type EngineComponentStatus, + type EntityType, +} from '../../../../../common/api/entity_analytics'; import { getEntitiesIndexName } from '../utils'; import { createOrUpdateIndex } from '../../utils/create_or_update_index'; @@ -36,3 +40,21 @@ export const deleteEntityIndex = ({ entityType, esClient, namespace }: Options) ignore: [404], } ); + +export const getEntityIndexStatus = async ({ + entityType, + esClient, + namespace, +}: Pick): Promise => { + const index = getEntitiesIndexName(entityType, namespace); + const exists = await esClient.indices.exists( + { + index, + }, + { + ignore: [404], + } + ); + + return { id: index, installed: exists, resource: EngineComponentResourceEnum.index }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/ingest_pipeline.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/ingest_pipeline.ts index f4d2b848b726f..f57102fd13f14 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/ingest_pipeline.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/ingest_pipeline.ts @@ -8,6 +8,7 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import type { IngestProcessorContainer } from '@elastic/elasticsearch/lib/api/types'; import type { EntityDefinition } from '@kbn/entities-schema'; +import { EngineComponentResourceEnum } from '../../../../../common/api/entity_analytics'; import { type FieldRetentionDefinition } from '../field_retention_definition'; import { debugDeepCopyContextStep, @@ -161,3 +162,27 @@ export const deletePlatformPipeline = ({ } ); }; + +export const getPlatformPipelineStatus = async ({ + definition, + esClient, +}: { + definition: EntityDefinition; + esClient: ElasticsearchClient; +}) => { + const pipelineId = getPlatformPipelineId(definition); + const pipeline = await esClient.ingest.getPipeline( + { + id: pipelineId, + }, + { + ignore: [404], + } + ); + + return { + id: pipelineId, + installed: !!pipeline[pipelineId], + resource: EngineComponentResourceEnum.ingest_pipeline, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts index a89469acf569b..72d650846fc55 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts @@ -20,19 +20,28 @@ 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 { EntityDefinitionWithState } from '@kbn/entityManager-plugin/server/lib/entities/types'; +import type { EntityDefinition } from '@kbn/entities-schema'; +import type { estypes } from '@elastic/elasticsearch'; import type { + GetEntityStoreStatusRequestQuery, GetEntityStoreStatusResponse, +} from '../../../../common/api/entity_analytics/entity_store/status.gen'; +import type { InitEntityStoreRequestBody, InitEntityStoreResponse, -} from '../../../../common/api/entity_analytics/entity_store/enablement.gen'; +} from '../../../../common/api/entity_analytics/entity_store/enable.gen'; import type { AppClient } from '../../..'; -import { EntityType } from '../../../../common/api/entity_analytics'; +import { EngineComponentResourceEnum, EntityType } from '../../../../common/api/entity_analytics'; import type { Entity, EngineDataviewUpdateResult, InitEntityEngineRequestBody, InitEntityEngineResponse, InspectQuery, + ListEntityEnginesResponse, + EngineComponentStatus, + EngineComponentResource, } from '../../../../common/api/entity_analytics'; import { EngineDescriptorClient } from './saved_object/engine_descriptor'; import { ENGINE_STATUS, ENTITY_STORE_STATUS, MAX_SEARCH_RESPONSE_SIZE } from './constants'; @@ -41,6 +50,7 @@ import { getUnitedEntityDefinition } from './united_entity_definitions'; import { startEntityStoreFieldRetentionEnrichTask, removeEntityStoreFieldRetentionEnrichTask, + getEntityStoreFieldRetentionEnrichTaskState as getEntityStoreFieldRetentionEnrichTaskStatus, } from './task'; import { createEntityIndex, @@ -52,6 +62,10 @@ import { createFieldRetentionEnrichPolicy, executeFieldRetentionEnrichPolicy, deleteFieldRetentionEnrichPolicy, + getPlatformPipelineStatus, + getFieldRetentionEnrichPolicyStatus, + getEntityIndexStatus, + getEntityIndexComponentTemplateStatus, } from './elasticsearch_assets'; import { RiskScoreDataClient } from '../risk_score/risk_score_data_client'; import { @@ -62,7 +76,6 @@ import { isPromiseRejected, } from './utils'; import { EntityEngineActions } from './auditing/actions'; -import { EntityStoreResource } from './auditing/resources'; import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../audit'; import type { EntityRecord, EntityStoreConfig } from './types'; import { @@ -71,6 +84,19 @@ import { } from '../../telemetry/event_based/events'; import { CRITICALITY_VALUES } from '../asset_criticality/constants'; +// Workaround. TransformState type is wrong. The health type should be: TransformHealth from '@kbn/transform-plugin/common/types/transform_stats' +export interface TransformHealth extends estypes.TransformGetTransformStatsTransformStatsHealth { + issues?: TransformHealthIssue[]; +} + +export interface TransformHealthIssue { + type: string; + issue: string; + details?: string; + count: number; + first_occurrence?: number; +} + interface EntityStoreClientOpts { logger: Logger; clusterClient: IScopedClusterClient; @@ -131,6 +157,42 @@ export class EntityStoreDataClient { }); } + private async getEngineComponentsState( + type: EntityType, + definition?: EntityDefinition + ): Promise { + const { namespace, taskManager } = this.options; + + return definition + ? Promise.all([ + ...(taskManager + ? [getEntityStoreFieldRetentionEnrichTaskStatus({ namespace, taskManager })] + : []), + getPlatformPipelineStatus({ + definition, + esClient: this.esClient, + }), + getFieldRetentionEnrichPolicyStatus({ + definitionMetadata: { + namespace, + entityType: type, + version: definition.version, + }, + esClient: this.esClient, + }), + getEntityIndexStatus({ + entityType: type, + esClient: this.esClient, + namespace, + }), + getEntityIndexComponentTemplateStatus({ + definitionId: definition.id, + esClient: this.esClient, + }), + ]) + : Promise.resolve([] as EngineComponentStatus[]); + } + public async enable( { indexPattern = '', filter = '', fieldHistoryLength = 10 }: InitEntityStoreRequestBody, { pipelineDebugMode = false }: { pipelineDebugMode?: boolean } = {} @@ -152,7 +214,10 @@ export class EntityStoreDataClient { return { engines, succeeded: true }; } - public async status(): Promise { + public async status({ + include_components: withComponents = false, + }: GetEntityStoreStatusRequestQuery): Promise { + const { namespace } = this.options; const { engines, count } = await this.engineClient.list(); let status = ENTITY_STORE_STATUS.RUNNING; @@ -166,7 +231,38 @@ export class EntityStoreDataClient { status = ENTITY_STORE_STATUS.INSTALLING; } - return { engines, status }; + if (withComponents) { + const enginesWithComponents = await Promise.all( + engines.map(async (engine) => { + const entityDefinitionId = buildEntityDefinitionId(engine.type, namespace); + const { + definitions: [definition], + } = await this.entityClient.getEntityDefinitions({ + id: entityDefinitionId, + includeState: withComponents, + }); + + const definitionComponents = this.getComponentFromEntityDefinition( + entityDefinitionId, + definition + ); + + const entityStoreComponents = await this.getEngineComponentsState( + engine.type, + definition + ); + + return { + ...engine, + components: [...definitionComponents, ...entityStoreComponents], + }; + }) + ); + + return { engines: enginesWithComponents, status }; + } else { + return { engines, status }; + } } public async init( @@ -203,7 +299,7 @@ export class EntityStoreDataClient { this.log('info', entityType, `Initializing entity store`); this.audit( EntityEngineActions.INIT, - EntityStoreResource.ENTITY_ENGINE, + EngineComponentResourceEnum.entity_engine, entityType, 'Initializing entity engine' ); @@ -332,7 +428,7 @@ export class EntityStoreDataClient { this.audit( EntityEngineActions.INIT, - EntityStoreResource.ENTITY_ENGINE, + EngineComponentResourceEnum.entity_engine, entityType, 'Failed to initialize entity engine resources', err @@ -355,6 +451,50 @@ export class EntityStoreDataClient { } } + public getComponentFromEntityDefinition( + id: string, + definition: EntityDefinitionWithState | EntityDefinition + ): EngineComponentStatus[] { + if (!definition) { + return [ + { + id, + installed: false, + resource: EngineComponentResourceEnum.entity_definition, + }, + ]; + } + + if ('state' in definition) { + return [ + { + id: definition.id, + installed: definition.state.installed, + resource: EngineComponentResourceEnum.entity_definition, + }, + ...definition.state.components.transforms.map(({ installed, running, stats }) => ({ + id, + resource: EngineComponentResourceEnum.transform, + installed, + errors: (stats?.health as TransformHealth)?.issues?.map(({ issue, details }) => ({ + title: issue, + message: details, + })), + })), + ...definition.state.components.ingestPipelines.map((pipeline) => ({ + resource: EngineComponentResourceEnum.ingest_pipeline, + ...pipeline, + })), + ...definition.state.components.indexTemplates.map(({ installed }) => ({ + id, + installed, + resource: EngineComponentResourceEnum.index_template, + })), + ]; + } + return []; + } + public async getExistingEntityDefinition(entityType: EntityType) { const entityDefinitionId = buildEntityDefinitionId(entityType, this.options.namespace); @@ -387,7 +527,7 @@ export class EntityStoreDataClient { const fullEntityDefinition = await this.getExistingEntityDefinition(entityType); this.audit( EntityEngineActions.START, - EntityStoreResource.ENTITY_DEFINITION, + EngineComponentResourceEnum.entity_definition, entityType, 'Starting entity definition' ); @@ -414,7 +554,7 @@ export class EntityStoreDataClient { const fullEntityDefinition = await this.getExistingEntityDefinition(entityType); this.audit( EntityEngineActions.STOP, - EntityStoreResource.ENTITY_DEFINITION, + EngineComponentResourceEnum.entity_definition, entityType, 'Stopping entity definition' ); @@ -428,7 +568,7 @@ export class EntityStoreDataClient { return this.engineClient.get(entityType); } - public async list() { + public async list(): Promise { return this.engineClient.list(); } @@ -457,7 +597,7 @@ export class EntityStoreDataClient { this.log('info', entityType, `Deleting entity store`); this.audit( EntityEngineActions.DELETE, - EntityStoreResource.ENTITY_ENGINE, + EngineComponentResourceEnum.entity_engine, entityType, 'Deleting entity engine' ); @@ -525,7 +665,7 @@ export class EntityStoreDataClient { this.audit( EntityEngineActions.DELETE, - EntityStoreResource.ENTITY_ENGINE, + EngineComponentResourceEnum.entity_engine, entityType, 'Failed to delete entity engine', err @@ -672,7 +812,7 @@ export class EntityStoreDataClient { private audit( action: EntityEngineActions, - resource: EntityStoreResource, + resource: EngineComponentResource, entityType: EntityType, msg: string, error?: Error diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/enablement.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/enablement.ts index 16813fccdf235..d19e9699f4756 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/enablement.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/enablement.ts @@ -10,8 +10,8 @@ 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 type { InitEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/enable.gen'; +import { InitEntityStoreRequestBody } from '../../../../../common/api/entity_analytics/entity_store/enable.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'; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stats.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stats.ts deleted file mode 100644 index 24785fbd5c015..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stats.ts +++ /dev/null @@ -1,64 +0,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 { buildRouteValidationWithZod } from '@kbn/zod-helpers'; - -import type { GetEntityEngineStatsResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/stats.gen'; -import { GetEntityEngineStatsRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/stats.gen'; -import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; -import type { EntityAnalyticsRoutesDeps } from '../../types'; - -export const getEntityEngineStatsRoute = ( - router: EntityAnalyticsRoutesDeps['router'], - logger: Logger -) => { - router.versioned - .post({ - access: 'public', - path: '/api/entity_store/engines/{entityType}/stats', - security: { - authz: { - requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`], - }, - }, - }) - .addVersion( - { - version: API_VERSIONS.public.v1, - validate: { - request: { - params: buildRouteValidationWithZod(GetEntityEngineStatsRequestParams), - }, - }, - }, - - async ( - context, - request, - response - ): Promise> => { - const siemResponse = buildSiemResponse(response); - - try { - // TODO - throw new Error('Not implemented'); - - // return response.ok({ body }); - } catch (e) { - logger.error('Error in GetEntityEngineStats:', e); - const error = transformError(e); - return siemResponse.error({ - statusCode: error.statusCode, - body: error.message, - }); - } - } - ); -}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/status.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/status.ts index 7a59b59b9914a..c2c24b114f66b 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/status.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/status.ts @@ -15,7 +15,9 @@ 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 { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/status.gen'; +import { GetEntityStoreStatusRequestQuery } from '../../../../../common/api/entity_analytics/entity_store/status.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'; @@ -38,7 +40,11 @@ export const getEntityStoreStatusRoute = ( .addVersion( { version: API_VERSIONS.public.v1, - validate: {}, + validate: { + request: { + query: buildRouteValidationWithZod(GetEntityStoreStatusRequestQuery), + }, + }, }, async ( @@ -54,7 +60,7 @@ export const getEntityStoreStatusRoute = ( try { const body: GetEntityStoreStatusResponse = await secSol .getEntityStoreDataClient() - .status(); + .status(request.query); return response.ok({ body }); } catch (e) { diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/task/field_retention_enrichment_task.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/task/field_retention_enrichment_task.ts index 708b74277faae..3d103ea2af467 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/task/field_retention_enrichment_task.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/task/field_retention_enrichment_task.ts @@ -13,7 +13,10 @@ import type { TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; -import type { EntityType } from '../../../../../common/api/entity_analytics/entity_store'; +import { + EngineComponentResourceEnum, + type EntityType, +} from '../../../../../common/api/entity_analytics/entity_store'; import { defaultState, stateSchemaByVersion, @@ -275,3 +278,37 @@ const createTaskRunnerFactory = }, }; }; + +export const getEntityStoreFieldRetentionEnrichTaskState = async ({ + namespace, + taskManager, +}: { + namespace: string; + taskManager: TaskManagerStartContract; +}) => { + const taskId = getTaskId(namespace); + try { + const taskState = await taskManager.get(taskId); + + return { + id: taskState.id, + resource: EngineComponentResourceEnum.task, + installed: true, + enabled: taskState.enabled, + status: taskState.status, + retryAttempts: taskState.attempts, + nextRun: taskState.runAt, + lastRun: taskState.state.lastExecutionTimestamp, + runs: taskState.state.runs, + }; + } catch (e) { + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + return { + id: taskId, + installed: false, + resource: EngineComponentResourceEnum.task, + }; + } + throw e; + } +}; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 97415cc236720..45282c5713e11 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -40397,7 +40397,6 @@ "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.noPermissionTitle": "Privilèges d'index insuffisants pour effectuer le chargement de fichiers CSV", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.resultsStepTitle": "Résultats", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.selectFileStepTitle": "Sélectionner un fichier", - "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.subTitle": "Importer des entités à l'aide d'un fichier texte", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unavailable": "La fonctionnalité de chargement de fichiers CSV de criticité des ressources n'est pas disponible.", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unsupportedFileTypeError": "Format de fichier sélectionné non valide. Veuillez choisir un fichier {supportedFileExtensions} et réessayer", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.uploadFileSizeLimit": "La taille maximale de fichier est de : {maxFileSize}", @@ -40439,7 +40438,6 @@ "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.clearAllEntities": "Effacer toutes les entités", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.close": "Fermer", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.title": "Effacer les données des entités ?", - "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntityData": "Supprimez toutes les données d'entité extraites du stockage. Cette action supprimera définitivement les enregistrements d’utilisateur et d'hôte persistants, les données ne seront par ailleurs plus disponibles pour l'analyse. Procédez avec prudence, car cette opération est irréversible. Notez que cette opération ne supprimera pas les données sources, les scores de risque des entités ni les attributions de criticité des ressources.", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.featureFlagDisabled": "Les fonctionnalités du stockage d'entités ne sont pas disponibles", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.subTitle": "Permet un monitoring complet des hôtes et des utilisateurs de votre système.", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.title": "Stockage d'entités", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6fb9f64a18d05..9aa8aa0977b71 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -40366,7 +40366,6 @@ "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.noPermissionTitle": "CSVアップロードを実行する十分なインデックス権限がありません", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.resultsStepTitle": "結果", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.selectFileStepTitle": "ファイルを選択", - "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.subTitle": "テキストファイルを使用してエンティティをインポート", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unavailable": "アセット重要度CSVファイルアップロード機能が利用できません。", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unsupportedFileTypeError": "無効なファイル形式が選択されました。{supportedFileExtensions}ファイルを選択して再試行してください。", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.uploadFileSizeLimit": "最大ファイルサイズ:{maxFileSize}", @@ -40408,7 +40407,6 @@ "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.clearAllEntities": "すべてのエンティティを消去", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.close": "閉じる", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.title": "エンティティデータを消去しますか?", - "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntityData": "すべての抽出されたエンティティデータをストアから削除します。このアクションにより、永続化されたユーザーおよびホストレコードが完全に削除され、データは分析で使用できなくなります。注意して続行してください。この操作は元に戻せません。この処理では、ソースデータ、エンティティリスクスコア、アセット重要度割り当ては削除されません。", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.featureFlagDisabled": "エンティティストア機能を利用できません", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.subTitle": "システムのホストとユーザーを包括的に監視できます。", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.title": "エンティティストア", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9d4eb17c385d7..389ccb9e5999f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -39738,7 +39738,6 @@ "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.noPermissionTitle": "索引权限不足,无法执行 CSV 上传", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.resultsStepTitle": "结果", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.selectFileStepTitle": "选择文件", - "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.subTitle": "使用文本文件导入实体", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unavailable": "资产关键度 CSV 文件上传功能不可用。", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unsupportedFileTypeError": "选定的文件格式无效。请选择 {supportedFileExtensions} 文件,然后重试", "xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.uploadFileSizeLimit": "最大文件大小:{maxFileSize}", @@ -39780,7 +39779,6 @@ "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.clearAllEntities": "清除所有实体", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.close": "关闭", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.title": "清除实体数据?", - "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntityData": "移除从仓库中提取的所有实体数据。此操作将永久删除持久化用户和主机记录,并且数据将不再可用于分析。请谨慎操作,因为此操作无法撤消。请注意,此操作不会删除源数据、实体风险分数或资产关键度分配。", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.featureFlagDisabled": "实体仓库功能不可用", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.subTitle": "允许全面监测您系统的主机和用户。", "xpack.securitySolution.entityAnalytics.entityStoreManagementPage.title": "实体仓库", diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 3574199709aee..ed598c941ed56 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -80,7 +80,7 @@ import { GetEndpointSuggestionsRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/endpoint/suggestions/get_suggestions.gen'; import { GetEntityEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/get.gen'; -import { GetEntityEngineStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/stats.gen'; +import { GetEntityStoreStatusRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/status.gen'; import { GetNotesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_notes/get_notes_route.gen'; import { GetPolicyResponseRequestQueryInput } from '@kbn/security-solution-plugin/common/api/endpoint/policy/policy_response.gen'; import { GetProtectionUpdatesNoteRequestParamsInput } from '@kbn/security-solution-plugin/common/api/endpoint/protection_updates_note/protection_updates_note.gen'; @@ -106,7 +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 { InitEntityStoreRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/enable.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'; @@ -832,24 +832,13 @@ finalize it. .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, - getEntityEngineStats(props: GetEntityEngineStatsProps, kibanaSpace: string = 'default') { - return supertest - .post( - routeWithNamespace( - replaceParams('/api/entity_store/engines/{entityType}/stats', props.params), - kibanaSpace - ) - ) - .set('kbn-xsrf', 'true') - .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') - .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); - }, - getEntityStoreStatus(kibanaSpace: string = 'default') { + getEntityStoreStatus(props: GetEntityStoreStatusProps, 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'); + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .query(props.query); }, /** * Get all notes for a given document. @@ -1615,8 +1604,8 @@ export interface GetEndpointSuggestionsProps { export interface GetEntityEngineProps { params: GetEntityEngineRequestParamsInput; } -export interface GetEntityEngineStatsProps { - params: GetEntityEngineStatsRequestParamsInput; +export interface GetEntityStoreStatusProps { + query: GetEntityStoreStatusRequestQueryInput; } export interface GetNotesProps { query: GetNotesRequestQueryInput; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts index 5d0dcec11d9b6..104fbf05b5159 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts @@ -5,7 +5,7 @@ * 2.0. */ -import expect from '@kbn/expect'; +import expect from 'expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { EntityStoreUtils } from '../../utils'; import { dataViewRouteHelpersFactory } from '../../utils/data_view'; @@ -14,8 +14,7 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const utils = EntityStoreUtils(getService); - // Failing: See https://github.com/elastic/kibana/issues/200758 - describe.skip('@ess @skipInServerlessMKI Entity Store APIs', () => { + describe('@ess @skipInServerlessMKI Entity Store APIs', () => { const dataView = dataViewRouteHelpersFactory(supertest); before(async () => { @@ -85,7 +84,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - expect(getResponse.body).to.eql({ + expect(getResponse.body).toEqual({ status: 'started', type: 'host', indexPattern: '', @@ -101,7 +100,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - expect(getResponse.body).to.eql({ + expect(getResponse.body).toEqual({ status: 'started', type: 'user', indexPattern: '', @@ -118,7 +117,7 @@ export default ({ getService }: FtrProviderContext) => { // @ts-expect-error body is any const sortedEngines = body.engines.sort((a, b) => a.type.localeCompare(b.type)); - expect(sortedEngines).to.eql([ + expect(sortedEngines).toEqual([ { status: 'started', type: 'host', @@ -160,7 +159,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - expect(body.status).to.eql('stopped'); + expect(body.status).toEqual('stopped'); }); it('should start the entity engine', async () => { @@ -176,7 +175,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - expect(body.status).to.eql('started'); + expect(body.status).toEqual('started'); }); }); @@ -213,10 +212,11 @@ export default ({ getService }: FtrProviderContext) => { afterEach(async () => { await utils.cleanEngines(); }); + it('should return "not_installed" when no engines have been initialized', async () => { - const { body } = await api.getEntityStoreStatus().expect(200); + const { body } = await api.getEntityStoreStatus({ query: {} }).expect(200); - expect(body).to.eql({ + expect(body).toEqual({ engines: [], status: 'not_installed', }); @@ -225,23 +225,56 @@ export default ({ getService }: FtrProviderContext) => { it('should return "installing" when at least one engine is being initialized', async () => { await utils.enableEntityStore(); - const { body } = await api.getEntityStoreStatus().expect(200); + const { body } = await api.getEntityStoreStatus({ query: {} }).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'); + expect(body.status).toEqual('installing'); + expect(body.engines.length).toEqual(2); + expect(body.engines[0].status).toEqual('installing'); + expect(body.engines[1].status).toEqual('installing'); }); it('should return "started" when all engines are started', async () => { await utils.initEntityEngineForEntityTypesAndWait(['host', 'user']); - const { body } = await api.getEntityStoreStatus().expect(200); + const { body } = await api.getEntityStoreStatus({ query: {} }).expect(200); + + expect(body.status).toEqual('running'); + expect(body.engines.length).toEqual(2); + expect(body.engines[0].status).toEqual('started'); + expect(body.engines[1].status).toEqual('started'); + }); - 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('status with components', () => { + it('should return empty list when when no engines have been initialized', async () => { + const { body } = await api + .getEntityStoreStatus({ query: { include_components: true } }) + .expect(200); + + expect(body).toEqual({ + engines: [], + status: 'not_installed', + }); + }); + + it('should return components status when engines are installed', async () => { + await utils.initEntityEngineForEntityTypesAndWait(['host']); + + const { body } = await api + .getEntityStoreStatus({ query: { include_components: true } }) + .expect(200); + + expect(body.engines[0].components).toEqual([ + expect.objectContaining({ resource: 'entity_definition' }), + expect.objectContaining({ resource: 'transform' }), + expect.objectContaining({ resource: 'ingest_pipeline' }), + expect.objectContaining({ resource: 'index_template' }), + expect.objectContaining({ resource: 'task' }), + expect.objectContaining({ resource: 'ingest_pipeline' }), + expect.objectContaining({ resource: 'enrich_policy' }), + expect.objectContaining({ resource: 'index' }), + expect.objectContaining({ resource: 'component_template' }), + ]); + }); }); }); @@ -262,14 +295,14 @@ export default ({ getService }: FtrProviderContext) => { it("should not update the index patten when it didn't change", async () => { const response = await api.applyEntityEngineDataviewIndices(); - expect(response.body).to.eql({ success: true, result: [{ type: 'host', changes: {} }] }); + expect(response.body).toEqual({ success: true, result: [{ type: 'host', changes: {} }] }); }); it('should update the index pattern when the data view changes', async () => { await dataView.updateIndexPattern('security-solution', 'test-*'); const response = await api.applyEntityEngineDataviewIndices(); - expect(response.body).to.eql({ + expect(response.body).toEqual({ success: true, result: [ {