Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
* 2.0.
*/
import { useMemo } from 'react';
import type {
GetEntityStoreStatusResponse,
InitEntityStoreRequestBody,
InitEntityStoreResponse,
} from '../../../common/api/entity_analytics/entity_store/enablement.gen';
import type {
DeleteEntityEngineResponse,
EntityType,
Expand All @@ -20,15 +25,32 @@ export const useEntityStoreRoutes = () => {
const http = useKibana().services.http;

return useMemo(() => {
const initEntityStore = async (entityType: EntityType) => {
const enableEntityStore = async (
options: InitEntityStoreRequestBody = { fieldHistoryLength: 10 }
) => {
return http.fetch<InitEntityStoreResponse>('/api/entity_store/enable', {
method: 'POST',
version: API_VERSIONS.public.v1,
body: JSON.stringify(options),
});
};

const getEntityStoreStatus = async () => {
return http.fetch<GetEntityStoreStatusResponse>('/api/entity_store/status', {
method: 'GET',
version: API_VERSIONS.public.v1,
});
};

const initEntityEngine = async (entityType: EntityType) => {
return http.fetch<InitEntityEngineResponse>(`/api/entity_store/engines/${entityType}/init`, {
method: 'POST',
version: API_VERSIONS.public.v1,
body: JSON.stringify({}),
});
};

const stopEntityStore = async (entityType: EntityType) => {
const stopEntityEngine = async (entityType: EntityType) => {
return http.fetch<StopEntityEngineResponse>(`/api/entity_store/engines/${entityType}/stop`, {
method: 'POST',
version: API_VERSIONS.public.v1,
Expand Down Expand Up @@ -59,8 +81,10 @@ export const useEntityStoreRoutes = () => {
};

return {
initEntityStore,
stopEntityStore,
enableEntityStore,
getEntityStoreStatus,
initEntityEngine,
stopEntityEngine,
getEntityEngine,
deleteEntityEngine,
listEntityEngines,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const useIsNewRiskScoreModuleInstalled = (): RiskScoreModuleStatus => {
return { isLoading: false, installed: !!riskEngineStatus?.isNewRiskScoreModuleInstalled };
};

interface RiskEngineStatus extends RiskEngineStatusResponse {
export interface RiskEngineStatus extends RiskEngineStatusResponse {
isUpdateAvailable: boolean;
isNewRiskScoreModuleInstalled: boolean;
isNewRiskScoreModuleAvailable: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* 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, useState } from 'react';
import {
EuiCallOut,
EuiPanel,
EuiEmptyPrompt,
EuiLoadingLogo,
EuiToolTip,
EuiButton,
EuiImage,
} 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 { StoreStatus } from '../../../../../common/api/entity_analytics';
import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics';
import { useInitRiskEngineMutation } from '../../../api/hooks/use_init_risk_engine_mutation';
import { useEnableEntityStoreMutation } from '../hooks/use_entity_store';
import {
ENABLEMENT_INITIALIZING_RISK_ENGINE,
ENABLEMENT_INITIALIZING_ENTITY_STORE,
ENABLE_ALL_TITLE,
ENABLEMENT_DESCRIPTION_BOTH,
ENABLE_RISK_SCORE_TITLE,
ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY,
ENABLE_ENTITY_STORE_TITLE,
ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY,
} from '../translations';
import type { Enablements } from './enablement_modal';
import { EntityStoreEnablementModal } from './enablement_modal';
import dashboardEnableImg from '../../../images/entity_store_dashboard.png';
import type { RiskEngineStatus } from '../../../api/hooks/use_risk_engine_status';

interface EnableEntityStorePanelProps {
state: {
riskEngine: UseQueryResult<RiskEngineStatus>;
entityStore: UseQueryResult<GetEntityStoreStatusResponse>;
};
}

export const EnablementPanel: React.FC<EnableEntityStorePanelProps> = ({ state }) => {
const riskEngineStatus = state.riskEngine.data?.risk_engine_status;
const entityStoreStatus = state.entityStore.data?.status;

const [modal, setModalState] = useState({ visible: false });
const [riskEngineInitializing, setRiskEngineInitializing] = useState(false);

const initRiskEngine = useInitRiskEngineMutation();
const storeEnablement = useEnableEntityStoreMutation();

const enableEntityStore = useCallback(
(enable: Enablements) => () => {
if (enable.riskScore) {
const options = {
onSuccess: () => {
setRiskEngineInitializing(false);
if (enable.entityStore) {
storeEnablement.mutate();
}
},
};
setRiskEngineInitializing(true);
initRiskEngine.mutate(undefined, options);
setModalState({ visible: false });
return;
}

if (enable.entityStore) {
storeEnablement.mutate();
setModalState({ visible: false });
}
},
[storeEnablement, initRiskEngine]
);

if (storeEnablement.error) {
return (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.entityStore.enablement.mutation.errorTitle"
defaultMessage={'There was a problem initializing the entity store'}
/>
}
color="danger"
iconType="error"
>
<p>{storeEnablement.error.body.message}</p>
</EuiCallOut>
</>
);
}

if (riskEngineInitializing) {
return (
<EuiPanel hasBorder data-test-subj="riskEngineInitializingPanel">
<EuiEmptyPrompt
icon={<EuiLoadingLogo logo="logoElastic" size="xl" />}
title={<h2>{ENABLEMENT_INITIALIZING_RISK_ENGINE}</h2>}
/>
</EuiPanel>
);
}

if (entityStoreStatus === 'installing' || storeEnablement.isLoading) {
return (
<EuiPanel hasBorder data-test-subj="entityStoreInitializingPanel">
<EuiEmptyPrompt
icon={<EuiLoadingLogo logo="logoElastic" size="xl" />}
title={<h2>{ENABLEMENT_INITIALIZING_ENTITY_STORE}</h2>}
body={
<p>
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.entityStore.enablement.initializing.description"
defaultMessage="This can take up to 5 minutes."
/>
</p>
}
/>
</EuiPanel>
);
}

if (
riskEngineStatus !== RiskEngineStatusEnum.NOT_INSTALLED &&
(entityStoreStatus === 'running' || entityStoreStatus === 'stopped')
) {
return null;
}

const [title, body] = getEnablementTexts(entityStoreStatus, riskEngineStatus);
return (
<>
<EuiEmptyPrompt
css={{ minWidth: '100%' }}
hasBorder
layout="horizontal"
title={<h2>{title}</h2>}
body={<p>{body}</p>}
actions={
<EuiToolTip content={title}>
<EuiButton
color="primary"
fill
onClick={() => setModalState({ visible: true })}
data-test-subj={`entityStoreEnablementButton`}
>
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.entityStore.enablement.enableButton"
defaultMessage="Enable"
/>
</EuiButton>
</EuiToolTip>
}
icon={<EuiImage size="l" hasShadow src={dashboardEnableImg} alt={title} />}
data-test-subj="entityStoreEnablementPanel"
/>

<EntityStoreEnablementModal
visible={modal.visible}
toggle={(visible) => setModalState({ visible })}
enableStore={enableEntityStore}
riskScore={{
disabled: riskEngineStatus !== RiskEngineStatusEnum.NOT_INSTALLED,
checked: riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED,
}}
entityStore={{
disabled: entityStoreStatus === 'running',
checked: entityStoreStatus === 'not_installed',
}}
/>
</>
);
};

const getEnablementTexts = (
entityStoreStatus?: StoreStatus,
riskEngineStatus?: RiskEngineStatus['risk_engine_status']
): [string, string] => {
if (
(entityStoreStatus === 'not_installed' || entityStoreStatus === 'stopped') &&
riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED
) {
return [ENABLE_ALL_TITLE, ENABLEMENT_DESCRIPTION_BOTH];
}

if (riskEngineStatus === RiskEngineStatusEnum.NOT_INSTALLED) {
return [ENABLE_RISK_SCORE_TITLE, ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY];
}

return [ENABLE_ENTITY_STORE_TITLE, ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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 {
EuiEmptyPrompt,
EuiLoadingSpinner,
EuiFlexItem,
EuiFlexGroup,
EuiPanel,
EuiCallOut,
} from '@elastic/eui';

import { FormattedMessage } from '@kbn/i18n-react';
import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics';
import { RiskScoreEntity } from '../../../../../common/search_strategy';
import { EntitiesList } from '../entities_list';
import { useEntityStoreStatus } from '../hooks/use_entity_store';
import { EntityAnalyticsRiskScores } from '../../entity_analytics_risk_score';
import { useRiskEngineStatus } from '../../../api/hooks/use_risk_engine_status';

import { EnablementPanel } from './dashboard_enablement_panel';

const EntityStoreDashboardPanelsComponent = () => {
const riskEngineStatus = useRiskEngineStatus();
const storeStatusQuery = useEntityStoreStatus({});

const callouts = (storeStatusQuery.data?.engines ?? [])
.filter((engine) => engine.status === 'error')
.map((engine) => {
const err = engine.error as {
message: string;
};
return (
<EuiCallOut
title={
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.entityStore.enablement.errors.title"
defaultMessage={'An error occurred during entity store resource initialization'}
/>
}
color="danger"
iconType="error"
>
<p>{err?.message}</p>
</EuiCallOut>
);
});

if (storeStatusQuery.status === 'loading') {
return (
<EuiPanel hasBorder>
<EuiEmptyPrompt icon={<EuiLoadingSpinner size="xl" />} />
</EuiPanel>
);
}

return (
<EuiFlexGroup direction="column" data-test-subj="entityStorePanelsGroup">
{storeStatusQuery.status === 'error' ? (
callouts
) : (
<EnablementPanel
state={{
riskEngine: riskEngineStatus,
entityStore: storeStatusQuery,
}}
/>
)}

{riskEngineStatus.data?.risk_engine_status !== RiskEngineStatusEnum.NOT_INSTALLED && (
<>
<EuiFlexItem>
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.user} />
</EuiFlexItem>
<EuiFlexItem>
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.host} />
</EuiFlexItem>
</>
)}
{storeStatusQuery.data?.status !== 'not_installed' &&
storeStatusQuery.data?.status !== 'installing' && (
<EuiFlexItem data-test-subj="entitiesListPanel">
<EntitiesList />
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};

export const EntityStoreDashboardPanels = React.memo(EntityStoreDashboardPanelsComponent);
EntityStoreDashboardPanels.displayName = 'EntityStoreDashboardPanels';
Loading