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/list.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.gen.ts index 089b70ce178cf..0a35fa49f6b33 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.gen.ts @@ -15,19 +15,9 @@ */ import { z } from '@kbn/zod'; -import { BooleanFromString } from '@kbn/zod-helpers'; import { EngineDescriptor } from '../common.gen'; -export type ListEntityEnginesRequestQuery = z.infer; -export const ListEntityEnginesRequestQuery = z.object({ - /** - * If true returns the full status of the engine definition and its components - */ - includeStatus: BooleanFromString.optional(), -}); -export type ListEntityEnginesRequestQueryInput = z.input; - export type ListEntityEnginesResponse = z.infer; export const ListEntityEnginesResponse = z.object({ count: z.number().int().optional(), diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.schema.yaml index 4c4b374cdff32..187718d609103 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.schema.yaml @@ -9,12 +9,6 @@ paths: x-codegen-enabled: true operationId: ListEntityEngines summary: List the Entity Engines - parameters: - - name: includeStatus - in: query - schema: - type: boolean - description: If true returns the full status of the engine definition and its components responses: '200': 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..f4ac47b51033a --- /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 + */ + withComponents: 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..1ac67d192b0dd --- /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: withComponents + 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 b4c8ebe6db444..5e432d8f86cac 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, @@ -252,10 +251,7 @@ import type { InitEntityEngineRequestBodyInput, InitEntityEngineResponse, } from './entity_analytics/entity_store/engine/init.gen'; -import type { - ListEntityEnginesRequestQueryInput, - ListEntityEnginesResponse, -} from './entity_analytics/entity_store/engine/list.gen'; +import type { ListEntityEnginesResponse } from './entity_analytics/entity_store/engine/list.gen'; import type { StartEntityEngineRequestParamsInput, StartEntityEngineResponse, @@ -272,6 +268,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'; @@ -1311,7 +1311,7 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } - async getEntityStoreStatus() { + async getEntityStoreStatus(props: GetEntityStoreStatusProps) { this.log.info(`${new Date().toISOString()} Calling API GetEntityStoreStatus`); return this.kbnClient .request({ @@ -1320,6 +1320,8 @@ finalize it. [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', }, method: 'GET', + + query: props.query, }) .catch(catchAxiosErrorFormatAndThrow); } @@ -1640,7 +1642,7 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } - async listEntityEngines(props: ListEntityEnginesProps) { + async listEntityEngines() { this.log.info(`${new Date().toISOString()} Calling API ListEntityEngines`); return this.kbnClient .request({ @@ -1649,8 +1651,6 @@ finalize it. [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', }, method: 'GET', - - query: props.query, }) .catch(catchAxiosErrorFormatAndThrow); } @@ -2299,6 +2299,9 @@ export interface GetEntityEngineProps { export interface GetEntityEngineStatsProps { params: GetEntityEngineStatsRequestParamsInput; } +export interface GetEntityStoreStatusProps { + query: GetEntityStoreStatusRequestQueryInput; +} export interface GetNotesProps { query: GetNotesRequestQueryInput; } @@ -2355,9 +2358,6 @@ export interface InternalUploadAssetCriticalityRecordsProps { export interface ListEntitiesProps { query: ListEntitiesRequestQueryInput; } -export interface ListEntityEnginesProps { - query: ListEntityEnginesRequestQueryInput; -} export interface PatchRuleProps { body: PatchRuleRequestBodyInput; } 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 6e7d3ce6c84d5..784d65618255a 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,6 +5,7 @@ * 2.0. */ import { useMemo } from 'react'; +import type { GetEntityStoreStatusResponse } from '../../../common/api/entity_analytics/entity_store/status.gen'; import type { DeleteEntityEngineResponse, EntityType, @@ -43,11 +44,18 @@ export const useEntityStoreRoutes = () => { }); }; - const listEntityEngines = async (includeStatus = false) => { + const listEntityEngines = async () => { return http.fetch(`/api/entity_store/engines`, { method: 'GET', version: API_VERSIONS.public.v1, - query: { includeStatus }, + }); + }; + + const getEntityStoreStatus = async (withComponents = false) => { + return http.fetch('/api/entity_store/status', { + method: 'GET', + version: API_VERSIONS.public.v1, + query: { withComponents }, }); }; @@ -56,6 +64,7 @@ export const useEntityStoreRoutes = () => { stopEntityStore, deleteEntityEngine, listEntityEngines, + getEntityStoreStatus, }; }, [http]); }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/diagnose_tool.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/diagnose_tool.tsx deleted file mode 100644 index 1e42dd53b5d12..0000000000000 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/diagnose_tool.tsx +++ /dev/null @@ -1,362 +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 { ReactNode } from 'react'; -import React, { useMemo, useState } from 'react'; - -import { - EuiButtonIcon, - EuiModal, - EuiModalHeader, - EuiModalBody, - EuiModalFooter, - EuiButton, - EuiModalHeaderTitle, - EuiLoadingSpinner, - EuiPanel, - EuiSpacer, - EuiAccordion, - EuiHealth, - EuiIcon, - EuiScreenReaderOnly, - EuiText, - EuiBadge, - EuiCodeBlock, - EuiLink, - EuiButtonEmpty, - EuiFlexItem, - EuiTitle, - EuiFlexGroup, -} from '@elastic/eui'; -import { camelCase, capitalize } from 'lodash/fp'; -import { downloadBlob } from '../../../../common/utils/download_blob'; -import { useKibana } from '../../../../common/lib/kibana'; -import { EntityStoreResource } from '../../../../../common/entity_analytics/entity_store/constants'; -import { BasicTable } from '../../../../common/components/ml/tables/basic_table'; -import { useEntityEngineList } from '../hooks/use_entity_engine_list'; - -export const DiagnoseTool: React.FC = () => { - const [isModalVisible, setIsModalVisible] = useState(false); - - const closeModal = () => setIsModalVisible(false); - const showModal = () => setIsModalVisible(true); - const { data, isLoading } = useEntityEngineList({ includeStatus: true }); - - console.log({ engines: data?.engines }); - const downloadJson = () => { - downloadBlob(new Blob([JSON.stringify(data)]), 'entity-store-diagnose.json'); - }; - // TODO i18n - - if (isLoading) return ; - - if (data?.count === 0) { - return 'No installation found'; - } - - return ( - <> - - {data?.count > 0 && ( - - - - - Download status - - - - - )} - - {(data?.engines ?? []).map((engine) => ( - <> - -

{`${capitalize(engine.type)} Store`}

-
- - - - - - - - - ))} -
-
- - ); -}; - -// i18n -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 TRANSFORM_HEALTH_COLOR = { - green: 'success', - unknown: 'subdued', - yellow: 'warning', - red: 'danger', -} as const; - -const DiagnoseTable = ({ components }: { components: object[] }) => { - const { getUrlForApp, navigateToApp, capabilities } = useKibana().services.application; - const [expandedItems, setExpandedItems] = useState([]); - - const itemIdToExpandedRowMap = expandedItems.reduce((acc, component) => { - if (component.resource === EntityStoreResource.TRANSFORM) { - acc[componentToId(component)] = ; - - // { - // "id": "entities-v1-latest-security_user_default", - // "resource": "transform", - // "installed": true, - // "running": true, - // "state": "started", - // "health": "red", - // "issues": [ - // { - // "title": "Privileges check failed", - // "description": "Cannot create transform [entities-v1-latest-security_user_default] because user noPrivileges lacks the required permissions [-*elastic-cloud-logs-*:[view_index_metadata], .asset-criticality.asset-criticality-default:[], .entities.v1.latest.noop:[], apm-*-transaction*:[view_index_metadata], auditbeat-*:[view_index_metadata], endgame-*:[view_index_metadata], filebeat-*:[view_index_metadata], logs-*:[view_index_metadata], packetbeat-*:[view_index_metadata], risk-score.risk-score-latest-default:[], traces-apm*:[view_index_metadata], winlogbeat-*:[view_index_metadata]]" - // }, - // { - // "title": "Transform indexer failed", - // "description": "org.elasticsearch.ElasticsearchSecurityException: Cannot start transform [entities-v1-latest-security_user_default] because user lacks required permissions, see privileges_check_failed issue for more details" - // } - // ] - // }, - } - - return acc; - }, {}); - - const onToggle = (component: object) => { - const isItemExpanded = expandedItems.includes(component); - - if (isItemExpanded) { - setExpandedItems(expandedItems.filter((item) => component !== item)); - } else { - setExpandedItems([...expandedItems, component]); - } - }; - - console.log({ itemIdToExpandedRowMap, expandedItems }); - // I18N - const diagnoseTableColumns = [ - { - field: 'resource', - name: 'Resource', - width: '20%', - render: (value: EntityStoreResource) => RESOURCE_TO_TEXT[value], - }, - { - field: 'id', - name: 'Identifier', - render: (value: string, { resource, installed }) => { - // const goToKibanaSettings = useCallback( - // () => navigateToApp('management', { path: '/kibana/settings' }), - // [navigateToApp] - // ); - if (!installed) { - return value; - } - - // size="s" - if (resource === EntityStoreResource.INGEST_PIPELINE) { - return ( - - {value} - - ); - } - - if (resource === EntityStoreResource.INDEX_TEMPLATE) { - return ( - - {value} - - ); - } - - if (resource === EntityStoreResource.INDEX) { - return ( - - {value} - - ); - } - - if (resource === EntityStoreResource.COMPONENT_TEMPLATE) { - return ( - - {value} - - ); - } - - if (resource === EntityStoreResource.ENRICH_POLICY) { - return ( - - {value} - - ); - } - - // http://localhost:5601/app/management/data/transform/enrich_policies?_a=(transform:(pageIndex:0,pageSize:10,queryText:'endpoint.metadata_current-default-8.16.0 ',showPerPageOptions:!t,sortDirection:asc,sortField:id)) - - if (resource === EntityStoreResource.TRANSFORM) { - return ( - - {value} - - ); - } - return value; - }, - }, - { - field: 'installed', - name: 'Installed', - width: '10%', - align: 'center', - render: (value: boolean) => - value ? ( - - ) : ( - - ), - }, - { - name: 'Health', - width: '10%', - align: 'center', - render: ({ installed, resource, health }) => { - if (!installed) { - return null; - } - - if (resource === EntityStoreResource.TRANSFORM) { - return ; - } - - return ; - }, - }, - { - isExpander: true, - align: 'right', - width: '40px', - name: ( - - Expand row - - ), - mobileOptions: { header: false }, - render: (component) => { - // const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - const isItemExpanded = expandedItems.includes(component); - - return component.resource === EntityStoreResource.TRANSFORM && - component.errors?.length > 0 ? ( - onToggle(component)} - // aria-label={itemIdToExpandedRowMapValues[user.id] ? 'Collapse' : 'Expand'} - // iconType={itemIdToExpandedRowMapValues[user.id] ? 'arrowDown' : 'arrowRight'} - iconType={isItemExpanded ? 'arrowDown' : 'arrowRight'} - /> - ) : null; - }, - }, - ]; - - return ( - - ); -}; - -const componentToId = ({ id, resource }) => `${resource}-${id}`; - -const TransformExtendedData = ({ transform }) => { - return ( - <> - {transform.errors?.map(({ title, description }) => ( - <> - - {title} - - - {description} - - ))} - - ); - // { - // "id": "entities-v1-latest-security_user_default", - // "resource": "transform", - // "installed": true, - // "running": true, - // "state": "started", - // "health": "red", - // "issues": [ - // { - // "title": "Privileges check failed", - // "description": "Cannot create transform [entities-v1-latest-security_user_default] because user noPrivileges lacks the required permissions [-*elastic-cloud-logs-*:[view_index_metadata], .asset-criticality.asset-criticality-default:[], .entities.v1.latest.noop:[], apm-*-transaction*:[view_index_metadata], auditbeat-*:[view_index_metadata], endgame-*:[view_index_metadata], filebeat-*:[view_index_metadata], logs-*:[view_index_metadata], packetbeat-*:[view_index_metadata], risk-score.risk-score-latest-default:[], traces-apm*:[view_index_metadata], winlogbeat-*:[view_index_metadata]]" - // }, - // { - // "title": "Transform indexer failed", - // "description": "org.elasticsearch.ElasticsearchSecurityException: Cannot start transform [entities-v1-latest-security_user_default] because user lacks required permissions, see privileges_check_failed issue for more details" - // } - // ] - // }, -}; 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..5f0f1df7fc464 --- /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 } 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..3fbaf6f6b1b82 --- /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.resource === EngineComponentResourceEnum.transform && + 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.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/index.tsx new file mode 100644 index 0000000000000..3bc6274db1a09 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/index.tsx @@ -0,0 +1,91 @@ +/* + * 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, { useCallback } from 'react'; +import { + EuiLoadingSpinner, + EuiPanel, + EuiSpacer, + EuiButtonEmpty, + EuiFlexItem, + EuiTitle, + EuiFlexGroup, +} from '@elastic/eui'; +import { capitalize } from 'lodash/fp'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { downloadBlob } from '../../../../../common/utils/download_blob'; +import { useEntityEngineStatusNew } from '../../hooks/use_entity_engine_status'; +import { EngineComponentsStatusTable } from './components/engine_components_status'; + +export const EngineStatus: React.FC = () => { + const { data, isLoading, error } = useEntityEngineStatusNew({ withComponents: true }); + + const downloadJson = useCallback(() => { + downloadBlob(new Blob([JSON.stringify(data)]), 'engines_status.json'); + }, [data]); + + if (!data || isLoading) return ; + + if (error) 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_engine_list.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_engine_list.ts deleted file mode 100644 index 228926f775edc..0000000000000 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_engine_list.ts +++ /dev/null @@ -1,35 +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 { UseQueryOptions } from '@tanstack/react-query'; -import { useQuery } from '@tanstack/react-query'; -import type { ListEntityEnginesResponse } from '../../../../../common/api/entity_analytics'; -import { useEntityStoreRoutes } from '../../../api/entity_store'; - -export const ENTITY_STORE_ENGINE_LIST = 'ENTITY_STORE_ENGINE_LIST'; - -interface Options { - includeStatus?: boolean; - disabled?: boolean; - polling?: UseQueryOptions['refetchInterval']; -} - -export const useEntityEngineList = (opts: Options = {}) => { - const { listEntityEngines } = useEntityStoreRoutes(); - - const { isLoading, data } = useQuery({ - queryKey: [ENTITY_STORE_ENGINE_LIST, opts.includeStatus], - queryFn: () => listEntityEngines(opts.includeStatus), - refetchInterval: opts.polling, - enabled: !opts.disabled, - }); - - return { - isLoading, - data, - }; -}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_engine_status.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_engine_status.ts index 8a1760728074b..eaaf0f080802d 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_engine_status.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/hooks/use_entity_engine_status.ts @@ -7,6 +7,8 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; +import type { IHttpFetchError } from '@kbn/core/public'; +import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/status.gen'; import type { ListEntityEnginesResponse } from '../../../../../common/api/entity_analytics'; import { useEntityStoreRoutes } from '../../../api/entity_store'; @@ -15,12 +17,26 @@ export const ENTITY_STORE_ENGINE_STATUS = 'ENTITY_STORE_ENGINE_STATUS'; interface Options { disabled?: boolean; polling?: UseQueryOptions['refetchInterval']; + withComponents?: boolean; } interface EngineError { message: string; } +export const useEntityEngineStatusNew = (opts: Options = {}) => { + const { getEntityStoreStatus } = useEntityStoreRoutes(); + + return useQuery({ + queryKey: ['GET_ENTITY_STORE_STATUS', opts.withComponents], + queryFn: () => getEntityStoreStatus(opts.withComponents), + cacheTime: 0, + // enabled: !skip, + refetchOnWindowFocus: true, + keepPreviousData: true, + }); +}; + export const useEntityEngineStatus = (opts: Options = {}) => { // QUESTION: Maybe we should have an `EnablementStatus` API route for this? const { listEntityEngines } = useEntityStoreRoutes(); 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 44596809f013c..23e1d13bda3ca 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,7 +21,6 @@ import { EuiCode, EuiSwitch, EuiHealth, - EuiButton, EuiLoadingSpinner, EuiToolTip, EuiBetaBadge, @@ -31,7 +30,7 @@ import { } from '@elastic/eui'; import React, { useCallback, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useEntityEngineStatus } from '../components/entity_store/hooks/use_entity_engine_status'; + import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { ASSET_CRITICALITY_INDEX_PATTERN } from '../../../common/entity_analytics/asset_criticality'; import { useKibana } from '../../common/lib/kibana'; @@ -46,7 +45,8 @@ 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 { DiagnoseTool } from '../components/entity_store/components/diagnose_tool'; +import { EngineStatus } from '../components/entity_store/components/engines_status'; +import { useEntityEngineStatus } from '../components/entity_store/hooks/use_entity_engine_status'; const entityStoreEnabledStatuses = ['enabled']; const switchDisabledStatuses = ['error', 'loading', 'installing']; @@ -314,25 +314,21 @@ export const EntityStoreManagementPage = () => { {'Import Entities'} - {/* {installedStatuses.includes(entityStoreStatus.status) && privileges?.has_all_required && ( */} - setSelectedTabId(TabId.Resources)} - > - {'Engine Status'} - - {/* )} */} + {installedStatuses.includes(entityStoreStatus.status) && privileges?.has_all_required && ( + setSelectedTabId(TabId.Resources)} + > + {'Engine Status'} + + )} {selectedTabId === TabId.Import && } - {selectedTabId === TabId.Resources && ( - // - - // - )} + {selectedTabId === TabId.Resources && } {initEntityEngineMutation.isError && ( diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/component_template.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/component_template.ts index 1a56c7861607c..f8e2de5a10749 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/component_template.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/component_template.ts @@ -6,6 +6,7 @@ */ import type { ElasticsearchClient } from '@kbn/core/server'; +import type { EngineComponentStatus } from '../../../../../common/api/entity_analytics'; import { EntityStoreResource } from '../../../../../common/entity_analytics/entity_store/constants'; import type { UnitedEntityDefinition } from '../united_entity_definitions'; @@ -43,10 +44,10 @@ export const deleteEntityIndexComponentTemplate = ({ unitedDefinition, esClient ); }; -export const getEntityIndexComponentTemplateState = async ({ +export const getEntityIndexComponentTemplateStatus = async ({ definitionId, esClient, -}: Pick & { definitionId: string }) => { +}: Pick & { definitionId: string }): Promise => { const name = getComponentTemplateName(definitionId); const componentTemplate = await esClient.cluster.getComponentTemplate( { 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 b80c98873e1a5..edf21219007e4 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 @@ -107,7 +107,7 @@ export const deleteFieldRetentionEnrichPolicy = async ({ } }; -export const getFieldRetentionEnrichPolicyState = async ({ +export const getFieldRetentionEnrichPolicyStatus = async ({ definitionMetadata, esClient, }: { 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 2a1a2c2b7d174..f000fc16d6639 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 @@ -7,7 +7,7 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { EntityStoreResource } from '../../../../../common/entity_analytics/entity_store/constants'; -import type { EntityType } from '../../../../../common/api/entity_analytics'; +import type { EngineComponentStatus, EntityType } from '../../../../../common/api/entity_analytics'; import { getEntitiesIndexName } from '../utils'; import { createOrUpdateIndex } from '../../utils/create_or_update_index'; @@ -38,11 +38,11 @@ export const deleteEntityIndex = ({ entityType, esClient, namespace }: Options) } ); -export const getEntityIndexState = async ({ +export const getEntityIndexStatus = async ({ entityType, esClient, namespace, -}: Pick) => { +}: Pick): Promise => { const index = getEntitiesIndexName(entityType, namespace); const exists = await esClient.indices.exists( { 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 91dbeb822ecff..da96b672d2f5a 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 @@ -163,7 +163,7 @@ export const deletePlatformPipeline = ({ ); }; -export const getPlatformPipelineState = async ({ +export const getPlatformPipelineStatus = async ({ definition, esClient, }: { 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 a689ad4441562..f0d6e33a1a30c 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 @@ -24,10 +24,13 @@ import type { EntityDefinitionWithState } from '@kbn/entityManager-plugin/server 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 { EntityStoreResource } from '../../../../common/entity_analytics/entity_store/constants'; import type { AppClient } from '../../..'; import { EntityType } from '../../../../common/api/entity_analytics'; @@ -38,6 +41,7 @@ import type { InitEntityEngineResponse, InspectQuery, ListEntityEnginesResponse, + EngineComponentStatus, } from '../../../../common/api/entity_analytics'; import { EngineDescriptorClient } from './saved_object/engine_descriptor'; import { ENGINE_STATUS, ENTITY_STORE_STATUS, MAX_SEARCH_RESPONSE_SIZE } from './constants'; @@ -46,7 +50,7 @@ import { getUnitedEntityDefinition } from './united_entity_definitions'; import { startEntityStoreFieldRetentionEnrichTask, removeEntityStoreFieldRetentionEnrichTask, - getEntityStoreFieldRetentionEnrichTaskState, + getEntityStoreFieldRetentionEnrichTaskState as getEntityStoreFieldRetentionEnrichTaskStatus, } from './task'; import { createEntityIndex, @@ -58,10 +62,10 @@ import { createFieldRetentionEnrichPolicy, executeFieldRetentionEnrichPolicy, deleteFieldRetentionEnrichPolicy, - getPlatformPipelineState, - getFieldRetentionEnrichPolicyState, - getEntityIndexState, - getEntityIndexComponentTemplateState, + getPlatformPipelineStatus, + getFieldRetentionEnrichPolicyStatus, + getEntityIndexStatus, + getEntityIndexComponentTemplateStatus, } from './elasticsearch_assets'; import { RiskScoreDataClient } from '../risk_score/risk_score_data_client'; import { @@ -153,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 } = {} @@ -174,7 +214,10 @@ export class EntityStoreDataClient { return { engines, succeeded: true }; } - public async status(): Promise { + public async status({ + withComponents = false, + }: GetEntityStoreStatusRequestQuery): Promise { + const { namespace } = this.options; const { engines, count } = await this.engineClient.list(); let status = ENTITY_STORE_STATUS.RUNNING; @@ -188,9 +231,51 @@ 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 }; + } } + // [x] TS types and openAPI + // [x] check if this change should be part of another API + // [x] fix UI types + // [x] refactor UI + // TODO test for error state + // TODO API tests + // TODO Unit tests? + // TODO create openAPI doc and defines returned types interface EntityState {} + // TODO does calling `GET kbn:/api/entity_store/engines/user` on a clean cluster kill Kibana? + // TODO investigate why we list engines for user without saved object privileges (see message from Mark) + public async init( entityType: EntityType, { indexPattern = '', filter = '', fieldHistoryLength = 10 }: InitEntityEngineRequestBody, @@ -380,13 +465,12 @@ export class EntityStoreDataClient { public getComponentFromEntityDefinition( id: string, definition: EntityDefinitionWithState | EntityDefinition - ) { + ): EngineComponentStatus[] { if (!definition) { return [ { id, installed: false, - running: false, resource: EntityStoreResource.ENTITY_DEFINITION, }, ]; @@ -397,26 +481,22 @@ export class EntityStoreDataClient { { id: definition.id, installed: definition.state.installed, - running: definition.state.running, resource: EntityStoreResource.ENTITY_DEFINITION, }, - ...definition.state.components.transforms.map(({ id, installed, running, stats }) => ({ + ...definition.state.components.transforms.map(({ installed, running, stats }) => ({ id, resource: EntityStoreResource.TRANSFORM, installed, - state: stats?.state, - health: stats?.health?.status, - running, errors: (stats?.health as TransformHealth)?.issues?.map(({ issue, details }) => ({ title: issue, - description: details, + message: details, })), })), ...definition.state.components.ingestPipelines.map((pipeline) => ({ resource: EntityStoreResource.INGEST_PIPELINE, ...pipeline, })), - ...definition.state.components.indexTemplates.map(({ id, installed }) => ({ + ...definition.state.components.indexTemplates.map(({ installed }) => ({ id, installed, resource: EntityStoreResource.INDEX_TEMPLATE, @@ -499,80 +579,8 @@ export class EntityStoreDataClient { return { engine: this.engineClient.get(entityType) }; } - public async list(includeState = false): Promise { - const engineList = await this.engineClient.list(); - - console.log('engineList', engineList); - - const { namespace, taskManager } = this.options; - - if (includeState) { - const enginesWithDefinitions = await Promise.all( - engineList.engines.map(async (engine) => { - const entityDefinitionId = buildEntityDefinitionId(engine.type, namespace); - const { - definitions: [definition], - } = await this.entityClient.getEntityDefinitions({ - id: entityDefinitionId, - includeState, - }); - - const definitionComponents = this.getComponentFromEntityDefinition( - entityDefinitionId, - definition - ); - - const entityStoreComponents = definition - ? await Promise.all([ - taskManager - ? await getEntityStoreFieldRetentionEnrichTaskState({ namespace, taskManager }) - : Promise.resolve(null), - await getPlatformPipelineState({ - definition, - esClient: this.esClient, - }), - await getFieldRetentionEnrichPolicyState({ - definitionMetadata: { - namespace, - entityType: engine.type, - version: definition.version, - }, - esClient: this.esClient, - }), - await getEntityIndexState({ - entityType: engine.type, - esClient: this.esClient, - namespace, - }), - getEntityIndexComponentTemplateState({ - definitionId: definition.id, - esClient: this.esClient, - }), - ]) - : []; - - // TODO TS types and openAPI - // TODO check if this change should be part of another API - // TODO API tests - // TODO Unit tests? - // TODO UI - // TODO create openAPI doc and defines returned types interface EntityState {} - // TODO does calling `GET kbn:/api/entity_store/engines/user` on a clean cluster kill Kibana? - // TODO investigate why we list engines for user without saved object privileges - - return { - ...engine, - state: { - components: [...definitionComponents, ...entityStoreComponents], - }, - }; - }) - ); - - return { engines: enginesWithDefinitions, count: engineList.count }; - } else { - return engineList; - } + public async list(): Promise { + return this.engineClient.list(); } public async delete( 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/list.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/list.ts index 71525d058fd67..519afdb9767bb 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/list.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/list.ts @@ -9,8 +9,6 @@ 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 { ListEntityEnginesRequestQuery } from '../../../../../common/api/entity_analytics/entity_store/engine/list.gen'; import type { ListEntityEnginesResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/list.gen'; import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; @@ -33,11 +31,7 @@ export const listEntityEnginesRoute = ( .addVersion( { version: API_VERSIONS.public.v1, - validate: { - request: { - query: buildRouteValidationWithZod(ListEntityEnginesRequestQuery), - }, - }, + validate: {}, }, async (context, request, response): Promise> => { @@ -46,7 +40,7 @@ export const listEntityEnginesRoute = ( try { const secSol = await context.securitySolution; // should includeStatus be snake case? - const body = await secSol.getEntityStoreDataClient().list(request.query.includeStatus); + const body = await secSol.getEntityStoreDataClient().list(); return response.ok({ body }); } catch (e) { 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) {