From a0a0051b271c05d2ebc895521de273401598af99 Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Fri, 11 Oct 2024 13:54:47 +0200 Subject: [PATCH] wip --- .../entity_analytics/api/entity_store.ts | 16 +- .../components/dashboard_panels.tsx | 155 ++++++++++++------ .../components/enablement_modal.tsx | 12 +- .../hooks/use_entity_engine_status.ts | 57 ++++--- .../entity_store/hooks/use_entity_store.ts | 88 +++------- .../pages/entity_analytics_dashboard.tsx | 1 - .../entity_store/routes/delete.ts | 5 +- 7 files changed, 188 insertions(+), 146 deletions(-) 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 ad2e3929b40d0..5f8723f7f929f 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,11 +5,13 @@ * 2.0. */ import { useMemo } from 'react'; -import { API_VERSIONS } from '../../../common/entity_analytics/constants'; import type { - EngineDescriptor, - EntityType, -} from '../../../common/api/entity_analytics/entity_store/common.gen'; + GetEntityEngineResponse, + InitEntityEngineResponse, + ListEntityEnginesResponse, +} from '../../../common/api/entity_analytics'; +import { API_VERSIONS } from '../../../common/entity_analytics/constants'; +import type { EntityType } from '../../../common/api/entity_analytics/entity_store/common.gen'; import { useKibana } from '../../common/lib/kibana/kibana_react'; export const useEntityStoreRoutes = () => { @@ -17,7 +19,7 @@ export const useEntityStoreRoutes = () => { return useMemo(() => { const initEntityStore = async (entityType: EntityType) => { - return http.fetch(`/api/entity_store/engines/${entityType}/init`, { + return http.fetch(`/api/entity_store/engines/${entityType}/init`, { method: 'POST', version: API_VERSIONS.public.v1, body: JSON.stringify({}), @@ -25,14 +27,14 @@ export const useEntityStoreRoutes = () => { }; const getEntityEngine = async (entityType: EntityType) => { - return http.fetch(`/api/entity_store/engines/${entityType}`, { + return http.fetch(`/api/entity_store/engines/${entityType}`, { method: 'GET', version: API_VERSIONS.public.v1, }); }; const listEntityEngines = async () => { - return http.fetch(`/api/entity_store/engines`, { + return http.fetch(`/api/entity_store/engines`, { method: 'GET', version: API_VERSIONS.public.v1, }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx index a0c84b94057be..c88bbe89b56ee 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/dashboard_panels.tsx @@ -12,58 +12,82 @@ import { EuiLoadingSpinner, EuiFlexItem, EuiFlexGroup, + EuiLoadingLogo, + EuiPanel, } from '@elastic/eui'; +import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics'; import { RiskScoreEntity } from '../../../../../common/search_strategy'; -import { useRefetchQueries } from '../../../../common/hooks/use_refetch_queries'; import { Panel } from '../../../../common/components/panel'; import { HeaderSection } from '../../../../common/components/header_section'; import { EntityAnalyticsLearnMoreLink } from '../../risk_score_onboarding/entity_analytics_doc_link'; import { EntitiesList } from '../entities_list'; -import { useEntityStore } from '../hooks/use_entity_store'; +import { useEntityStoreEnablement } from '../hooks/use_entity_store'; import { EntityStoreEnablementModal, type Enablements } from './enablement_modal'; -import { EnableRiskScore } from '../../enable_risk_score'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; + import { EntityAnalyticsRiskScores } from '../../entity_analytics_risk_score'; +import { useInitRiskEngineMutation } from '../../../api/hooks/use_init_risk_engine_mutation'; +import { useEntityEngineStatus } from '../hooks/use_entity_engine_status'; const EntityStoreDashboardPanelsComponent = () => { - const [modal, setModalState] = useState({ visible: false, enablements: {} }); - - const entityStore = useEntityStore(); + const [modal, setModalState] = useState({ visible: false }); + const [riskEngineInitializing, setRiskEngineInitializing] = useState(false); - // NOTE: Props needed only for current implementation of the EnableRiskScore component - const refreshPage = useRefetchQueries(); - const { deleteQuery, setQuery, from, to } = useGlobalTime(); + const entityStore = useEntityEngineStatus(); + const { enable: enableStore } = useEntityStoreEnablement(); + const { mutate: initRiskEngine } = useInitRiskEngineMutation(); const enableEntityStore = (enable: Enablements) => () => { - setModalState({ visible: false, enablements: enable }); + setModalState({ visible: false }); if (enable.riskScore) { - // return enableRiskScore().then(() => { - // if (enablements.entityStore) { - // entityStore.enablement.enableEntityStore(); - // } - // }); + const options = { + onSuccess: () => { + setRiskEngineInitializing(false); + if (enable.entityStore) { + enableStore(); + } + }, + }; + setRiskEngineInitializing(true); + initRiskEngine(undefined, options); } if (enable.entityStore) { - entityStore.enablement.enableEntityStore(); + enableStore(); } }; - if (entityStore.enablement.loading) { - return ; + if (entityStore.status === 'loading') { + return ( + + } + title={

{'Initializing store'}

} + /> +
+ ); } - const isRiskScoreEnabled = - entityStore.status.newRiskScore.installed || - entityStore.status.legacyHostRiskScore.isEnabled || - entityStore.status.legacyUserRiskScore.isEnabled; + if (entityStore.status === 'installing') { + return ( + + } + title={

{'Initializing store'}

} + /> +
+ ); + } + + const isRiskScoreAvailable = + entityStore.riskEngineStatus.data && + entityStore.riskEngineStatus.data.risk_engine_status !== RiskEngineStatusEnum.NOT_INSTALLED; return ( - {entityStore.status.entityStore.status === 'enabled' && isRiskScoreEnabled && ( + {entityStore.status === 'enabled' && isRiskScoreAvailable && ( <> @@ -77,50 +101,33 @@ const EntityStoreDashboardPanelsComponent = () => { )} - {entityStore.status.entityStore.status === 'enabled' && !isRiskScoreEnabled && ( + {entityStore.status === 'enabled' && !isRiskScoreAvailable && ( <> - {/* QUESTION: Should we have a separate component for enabling risk score? Current one seems too overloaded */} - - - setModalState({ visible: true })} + loading={riskEngineInitializing} /> + )} - {entityStore.status.entityStore.status === 'not_installed' && !isRiskScoreEnabled && ( + {entityStore.status === 'not_installed' && !isRiskScoreAvailable && ( // TODO: Move modal inside EnableEntityStore component, eliminating the onEnable prop in favour of forwarding the riskScoreEnabled status - - setModalState({ visible: true, enablements: { riskScore: true, entityStore: true } }) - } - /> + setModalState({ visible: true })} /> )} - {entityStore.status.entityStore.status === 'not_installed' && isRiskScoreEnabled && ( + {entityStore.status === 'not_installed' && isRiskScoreAvailable && ( <> setModalState({ visible: true, - enablements: { riskScore: false, entityStore: true }, }) } /> @@ -136,9 +143,13 @@ const EntityStoreDashboardPanelsComponent = () => { setModalState((prev) => ({ ...prev, visible }))} + toggle={(visible) => setModalState({ visible })} enableStore={enableEntityStore} - riskScore={modal.enablements} + riskScore={{ disabled: isRiskScoreAvailable, checked: !isRiskScoreAvailable }} + entityStore={{ + disabled: entityStore.status === 'enabled', + checked: entityStore.status !== 'enabled', + }} /> ); @@ -177,5 +188,49 @@ export const EnableEntityStore: React.FC = ({ onEnable } ); }; +interface EnableRiskEngineProps { + onEnable: () => void; + loading: boolean; +} + +export const EnableRiskScore: React.FC = ({ onEnable, loading }) => { + if (loading) { + return ( + + } + title={

{'Initializing risk engine'}

} + /> +
+ ); + } + return ( + + + {'Placeholder title'}} + body={ + <> + {'Placeholder text'} + + + } + actions={ + + + {'Enable'} + + + } + /> + + ); +}; + export const EntityStoreDashboardPanels = React.memo(EntityStoreDashboardPanelsComponent); EntityStoreDashboardPanels.displayName = 'EntityStoreDashboardPanels'; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx index 286cfc352ea26..16e472f646c4e 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_store/components/enablement_modal.tsx @@ -31,6 +31,10 @@ interface EntityStoreEnablementModalProps { disabled?: boolean; checked?: boolean; }; + entityStore: { + disabled?: boolean; + checked?: boolean; + }; } export const EntityStoreEnablementModal: React.FC = ({ @@ -38,10 +42,11 @@ export const EntityStoreEnablementModal: React.FC { const [enablements, setEnablements] = useState({ riskScore: !!riskScore.checked, - entityStore: true, + entityStore: !!entityStore.checked, }); if (!visible) { @@ -58,7 +63,7 @@ export const EntityStoreEnablementModal: React.FC setEnablements((prev) => ({ ...prev, riskScore: !prev.riskScore }))} /> @@ -66,7 +71,8 @@ export const EntityStoreEnablementModal: React.FC setEnablements((prev) => ({ ...prev, entityStore: !prev.entityStore })) } 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 85e1953b888f9..32b40c7a6bcc5 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 @@ -5,40 +5,57 @@ * 2.0. */ +import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; -import type { EngineDescriptor } from '../../../../../common/api/entity_analytics'; +import type { ListEntityEnginesResponse } from '../../../../../common/api/entity_analytics'; import { useEntityStoreRoutes } from '../../../api/entity_store'; +import { useRiskEngineStatus } from '../../../api/hooks/use_risk_engine_status'; const ENTITY_STORE_ENGINE_STATUS = 'ENTITY_STORE_ENGINE_STATUS'; -export const useEntityEngineStatus = () => { +interface Options { + disabled?: boolean; + polling?: UseQueryOptions['refetchInterval']; +} + +export const useEntityEngineStatus = (opts: Options = {}) => { + const riskEngineStatus = useRiskEngineStatus(); // QUESTION: Maybe we should have an `EnablementStatus` API route for this? const { listEntityEngines } = useEntityStoreRoutes(); - const { isLoading, data } = useQuery({ + const { isLoading, data } = useQuery({ queryKey: [ENTITY_STORE_ENGINE_STATUS], - queryFn: listEntityEngines, + queryFn: () => listEntityEngines(), + refetchInterval: opts.polling, + enabled: !opts.disabled, }); - if (isLoading) { - return { status: 'loading' }; - } + const status = (() => { + if (data?.count === 0) { + return 'not_installed'; + } + + if (data?.engines?.every((engine) => engine.status === 'stopped')) { + return 'stopped'; + } - if (!data) { - return { status: 'error' }; - } + if (data?.engines?.some((engine) => engine.status === 'installing')) { + return 'installing'; + } - if (data.length === 0) { - return { status: 'not_installed' }; - } + if (isLoading) { + return 'loading'; + } - if (data.every((engine) => engine.status === 'stopped')) { - return { status: 'stopped' }; - } + if (!data) { + return 'error'; + } - if (data.some((engine) => engine.status === 'installing')) { - return { status: 'installing' }; - } + return 'enabled'; + })(); - return { status: 'enabled' }; + return { + status, + riskEngineStatus, + }; }; 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 7ef8d1f8b97b2..8329cced1bc20 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 @@ -6,82 +6,42 @@ */ import { useQuery } from '@tanstack/react-query'; -import { useCallback } from 'react'; -import type { EngineDescriptor } from '../../../../../common/api/entity_analytics'; -import { - RiskScoreEntity, - getHostRiskIndex, - getUserRiskIndex, -} from '../../../../../common/search_strategy'; -import { useSpaceId } from '../../../../common/hooks/use_space_id'; -import { useIsNewRiskScoreModuleInstalled } from '../../../api/hooks/use_risk_engine_status'; -import { useRiskScoreFeatureStatus } from '../../../api/hooks/use_risk_score_feature_status'; +import { useCallback, useState } from 'react'; + import { useEntityEngineStatus } from './use_entity_engine_status'; import { useEntityStoreRoutes } from '../../../api/entity_store'; const ENTITY_STORE_ENABLEMENT_INIT = 'ENTITY_STORE_ENABLEMENT_INIT'; -const ENTITY_ENGINE_STATUS_QUERY = 'ENTITY_ENGINE_STATUS_QUERY'; -export const useEntityStoreStatus = () => { - const spaceId = useSpaceId(); - const newRiskScore = useIsNewRiskScoreModuleInstalled(); - const defaultIndices = - spaceId && !newRiskScore.isLoading && newRiskScore.installed !== undefined - ? { - host: getHostRiskIndex(spaceId, true, newRiskScore.installed), - user: getUserRiskIndex(spaceId, true, newRiskScore.installed), - } - : undefined; - const legacyUserRiskScore = useRiskScoreFeatureStatus(RiskScoreEntity.user, defaultIndices?.user); - const legacyHostRiskScore = useRiskScoreFeatureStatus(RiskScoreEntity.host, defaultIndices?.host); - - const entityStore = useEntityEngineStatus(); - - return { - entityStore, - legacyUserRiskScore, - legacyHostRiskScore, - newRiskScore, - }; -}; export const useEntityStoreEnablement = () => { - const { getEntityEngine, initEntityStore } = useEntityStoreRoutes(); - const { refetch: poll, data } = useQuery({ - queryKey: [ENTITY_ENGINE_STATUS_QUERY], - queryFn: () => { - return Promise.all([getEntityEngine('user')]); - }, - refetchInterval: (update) => { - const shouldStopPolling = update && update.some((engine) => engine.status === 'started'); - return shouldStopPolling ? false : 5000; // TODO: increase interval + const [polling, setPolling] = useState(false); + + useEntityEngineStatus({ + disabled: !polling, + polling: (data) => { + const shouldStopPolling = + data?.engines && + data.engines.length > 0 && + data.engines.every((engine) => engine.status === 'started'); + + if (shouldStopPolling) { + setPolling(false); + return false; + } + return 5000; }, - enabled: false, }); - const { refetch: initialize, fetchStatus: initFetchStatus } = useQuery({ + const { initEntityStore } = useEntityStoreRoutes(); + const { refetch: initialize } = useQuery({ queryKey: [ENTITY_STORE_ENABLEMENT_INIT], - queryFn: () => { - return Promise.all([initEntityStore('user')]); - }, + queryFn: () => Promise.all([initEntityStore('user')]), enabled: false, }); - const enableEntityStore = useCallback(() => initialize().then(poll), [initialize, poll]); - - return { - loading: - initFetchStatus === 'fetching' || data?.some((engine) => engine.status === 'installing'), - data, - enableEntityStore, - }; -}; - -export const useEntityStore = () => { - const status = useEntityStoreStatus(); - const enablement = useEntityStoreEnablement(); + const enable = useCallback(() => { + initialize().then(() => setPolling(true)); + }, [initialize]); - return { - status, - enablement, - }; + return { enable }; }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx index 314c5af38e691..8de643bb3495a 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import { RiskScoreEntity } from '../../../common/search_strategy'; import { ENTITY_ANALYTICS } from '../../app/translations'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { SecurityPageName } from '../../app/types'; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/delete.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/delete.ts index 0828f94852cf2..e11c9d3fa7b9d 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/delete.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/delete.ts @@ -56,7 +56,10 @@ export const deleteEntityEngineRoute = ( const secSol = await context.securitySolution; const body = await secSol .getEntityStoreDataClient() - .delete(request.params.entityType, taskManager, !!request.query.data); + .delete(request.params.entityType, taskManager, { + deleteData: !!request.query.data, + deleteEngine: true, + }); return response.ok({ body }); } catch (e) {