mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Entity Store] Enablement UI (#196076)
### Entity store enablement UI This PR adds a UI to enable the Entity Store. ### How to test 1. Enable `entityStoreEnabled` experimental feature flag 2. Navigate to `Security > Dashboards > Entity Analytics` 3. Work through the distinct flows to enable the store * For example, choose to enable risk score together with the store 4. Navigate to `Security > Manage > Entity Store` to start/stop the store 5. Validate that the appropriate transforms and pipelines have been initialized and have the correct status (for example, via the Stack Management UI) EDIT: Enablement flow screenshots: #### Enable both risk score and entity store  #### Enable Risk score only (Entity store already enabled)  #### Modal to choose what to enable  #### New Entity Store management page  --------- Co-authored-by: jaredburgettelastic <jared.burgett@elastic.co> Co-authored-by: machadoum <pablo.nevesmachado@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Mark Hopkin <mark.hopkin@elastic.co> Co-authored-by: natasha-moore-elastic <137783811+natasha-moore-elastic@users.noreply.github.com>
This commit is contained in:
parent
f0f1775632
commit
58b2c6ebde
45 changed files with 1543 additions and 424 deletions
|
@ -48009,6 +48009,7 @@ components:
|
|||
- started
|
||||
- stopped
|
||||
- updating
|
||||
- error
|
||||
type: string
|
||||
Security_Entity_Analytics_API_Entity:
|
||||
oneOf:
|
||||
|
|
|
@ -48009,6 +48009,7 @@ components:
|
|||
- started
|
||||
- stopped
|
||||
- updating
|
||||
- error
|
||||
type: string
|
||||
Security_Entity_Analytics_API_Entity:
|
||||
oneOf:
|
||||
|
|
|
@ -56775,6 +56775,7 @@ components:
|
|||
- started
|
||||
- stopped
|
||||
- updating
|
||||
- error
|
||||
type: string
|
||||
Security_Entity_Analytics_API_Entity:
|
||||
oneOf:
|
||||
|
|
|
@ -56775,6 +56775,7 @@ components:
|
|||
- started
|
||||
- stopped
|
||||
- updating
|
||||
- error
|
||||
type: string
|
||||
Security_Entity_Analytics_API_Entity:
|
||||
oneOf:
|
||||
|
|
|
@ -86,6 +86,7 @@ export enum SecurityPageName {
|
|||
entityAnalytics = 'entity_analytics',
|
||||
entityAnalyticsManagement = 'entity_analytics-management',
|
||||
entityAnalyticsAssetClassification = 'entity_analytics-asset-classification',
|
||||
entityAnalyticsEntityStoreManagement = 'entity_analytics-entity_store_management',
|
||||
coverageOverview = 'coverage-overview',
|
||||
notes = 'notes',
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export type IndexPattern = z.infer<typeof IndexPattern>;
|
|||
export const IndexPattern = z.string();
|
||||
|
||||
export type EngineStatus = z.infer<typeof EngineStatus>;
|
||||
export const EngineStatus = z.enum(['installing', 'started', 'stopped', 'updating']);
|
||||
export const EngineStatus = z.enum(['installing', 'started', 'stopped', 'updating', 'error']);
|
||||
export type EngineStatusEnum = typeof EngineStatus.enum;
|
||||
export const EngineStatusEnum = EngineStatus.enum;
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ components:
|
|||
- started
|
||||
- stopped
|
||||
- updating
|
||||
- error
|
||||
|
||||
IndexPattern:
|
||||
type: string
|
||||
|
|
|
@ -124,6 +124,8 @@ export const ENTITY_ANALYTICS_PATH = '/entity_analytics' as const;
|
|||
export const ENTITY_ANALYTICS_MANAGEMENT_PATH = `/entity_analytics_management` as const;
|
||||
export const ENTITY_ANALYTICS_ASSET_CRITICALITY_PATH =
|
||||
`/entity_analytics_asset_criticality` as const;
|
||||
export const ENTITY_ANALYTICS_ENTITY_STORE_MANAGEMENT_PATH =
|
||||
`/entity_analytics_entity_store` as const;
|
||||
export const APP_ALERTS_PATH = `${APP_PATH}${ALERTS_PATH}` as const;
|
||||
export const APP_CASES_PATH = `${APP_PATH}${CASES_PATH}` as const;
|
||||
export const APP_ENDPOINTS_PATH = `${APP_PATH}${ENDPOINTS_PATH}` as const;
|
||||
|
|
|
@ -806,6 +806,7 @@ components:
|
|||
- started
|
||||
- stopped
|
||||
- updating
|
||||
- error
|
||||
type: string
|
||||
Entity:
|
||||
oneOf:
|
||||
|
|
|
@ -806,6 +806,7 @@ components:
|
|||
- started
|
||||
- stopped
|
||||
- updating
|
||||
- error
|
||||
type: string
|
||||
Entity:
|
||||
oneOf:
|
||||
|
|
|
@ -50,7 +50,7 @@ export const CATEGORIES: Array<SeparatorLinkCategory<SolutionPageName>> = [
|
|||
type: LinkCategoryType.separator,
|
||||
linkIds: [
|
||||
SecurityPageName.entityAnalyticsManagement,
|
||||
SecurityPageName.entityAnalyticsAssetClassification,
|
||||
SecurityPageName.entityAnalyticsEntityStoreManagement,
|
||||
], // Linked from the management cards landing.
|
||||
},
|
||||
];
|
||||
|
|
|
@ -12,7 +12,7 @@ import * as i18n from './settings_translations';
|
|||
|
||||
const ENTITY_ANALYTICS_LINKS = [
|
||||
SecurityPageName.entityAnalyticsManagement,
|
||||
SecurityPageName.entityAnalyticsAssetClassification,
|
||||
SecurityPageName.entityAnalyticsEntityStoreManagement,
|
||||
];
|
||||
|
||||
export const createSettingsLinksFromManage = (manageLink: LinkItem): LinkItem[] => {
|
||||
|
|
|
@ -25,6 +25,10 @@ export const ENTITY_ANALYTICS_RISK_SCORE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ENTITY_STORE = i18n.translate('xpack.securitySolution.navigation.entityStore', {
|
||||
defaultMessage: 'Entity Store',
|
||||
});
|
||||
|
||||
export const NOTES = i18n.translate('xpack.securitySolution.navigation.notes', {
|
||||
defaultMessage: 'Notes',
|
||||
});
|
||||
|
|
|
@ -547,7 +547,7 @@ describe('Security links', () => {
|
|||
describe('isLinkUiSettingsAllowed', () => {
|
||||
const SETTING_KEY = 'test setting';
|
||||
const mockedLink: LinkItem = {
|
||||
id: SecurityPageName.entityAnalyticsAssetClassification,
|
||||
id: SecurityPageName.entityAnalyticsEntityStoreManagement,
|
||||
title: 'test title',
|
||||
path: '/test_path',
|
||||
};
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import type {
|
||||
DeleteEntityEngineResponse,
|
||||
EntityType,
|
||||
GetEntityEngineResponse,
|
||||
InitEntityEngineResponse,
|
||||
ListEntityEnginesResponse,
|
||||
StopEntityEngineResponse,
|
||||
} from '../../../common/api/entity_analytics';
|
||||
import { API_VERSIONS } from '../../../common/entity_analytics/constants';
|
||||
import { useKibana } from '../../common/lib/kibana/kibana_react';
|
||||
|
||||
export const useEntityStoreRoutes = () => {
|
||||
const http = useKibana().services.http;
|
||||
|
||||
return useMemo(() => {
|
||||
const initEntityStore = 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) => {
|
||||
return http.fetch<StopEntityEngineResponse>(`/api/entity_store/engines/${entityType}/stop`, {
|
||||
method: 'POST',
|
||||
version: API_VERSIONS.public.v1,
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
};
|
||||
|
||||
const getEntityEngine = async (entityType: EntityType) => {
|
||||
return http.fetch<GetEntityEngineResponse>(`/api/entity_store/engines/${entityType}`, {
|
||||
method: 'GET',
|
||||
version: API_VERSIONS.public.v1,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteEntityEngine = async (entityType: EntityType) => {
|
||||
return http.fetch<DeleteEntityEngineResponse>(`/api/entity_store/engines/${entityType}`, {
|
||||
method: 'DELETE',
|
||||
version: API_VERSIONS.public.v1,
|
||||
});
|
||||
};
|
||||
|
||||
const listEntityEngines = async () => {
|
||||
return http.fetch<ListEntityEnginesResponse>(`/api/entity_store/engines`, {
|
||||
method: 'GET',
|
||||
version: API_VERSIONS.public.v1,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
initEntityStore,
|
||||
stopEntityStore,
|
||||
getEntityEngine,
|
||||
deleteEntityEngine,
|
||||
listEntityEngines,
|
||||
};
|
||||
}, [http]);
|
||||
};
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiToolTip,
|
||||
EuiButton,
|
||||
EuiLoadingSpinner,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiLoadingLogo,
|
||||
EuiPanel,
|
||||
EuiImage,
|
||||
} 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 { useEntityStoreEnablement } from '../hooks/use_entity_store';
|
||||
import { EntityStoreEnablementModal, type Enablements } from './enablement_modal';
|
||||
|
||||
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';
|
||||
|
||||
import dashboardEnableImg from '../../../images/entity_store_dashboard.png';
|
||||
import {
|
||||
ENABLEMENT_DESCRIPTION_BOTH,
|
||||
ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY,
|
||||
ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY,
|
||||
ENABLEMENT_INITIALIZING_ENTITY_STORE,
|
||||
ENABLEMENT_INITIALIZING_RISK_ENGINE,
|
||||
ENABLE_ALL_TITLE,
|
||||
ENABLE_ENTITY_STORE_TITLE,
|
||||
ENABLE_RISK_SCORE_TITLE,
|
||||
} from '../translations';
|
||||
import { useRiskEngineStatus } from '../../../api/hooks/use_risk_engine_status';
|
||||
|
||||
const EntityStoreDashboardPanelsComponent = () => {
|
||||
const [modal, setModalState] = useState({ visible: false });
|
||||
const [riskEngineInitializing, setRiskEngineInitializing] = useState(false);
|
||||
|
||||
const entityStore = useEntityEngineStatus();
|
||||
const riskEngineStatus = useRiskEngineStatus();
|
||||
|
||||
const { enable: enableStore } = useEntityStoreEnablement();
|
||||
const { mutate: initRiskEngine } = useInitRiskEngineMutation();
|
||||
|
||||
const enableEntityStore = (enable: Enablements) => () => {
|
||||
setModalState({ visible: false });
|
||||
if (enable.riskScore) {
|
||||
const options = {
|
||||
onSuccess: () => {
|
||||
setRiskEngineInitializing(false);
|
||||
if (enable.entityStore) {
|
||||
enableStore();
|
||||
}
|
||||
},
|
||||
};
|
||||
setRiskEngineInitializing(true);
|
||||
initRiskEngine(undefined, options);
|
||||
}
|
||||
|
||||
if (enable.entityStore) {
|
||||
enableStore();
|
||||
}
|
||||
};
|
||||
|
||||
if (entityStore.status === 'loading') {
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
<EuiEmptyPrompt
|
||||
icon={<EuiLoadingSpinner size="xl" />}
|
||||
title={<h2>{ENABLEMENT_INITIALIZING_ENTITY_STORE}</h2>}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
if (entityStore.status === 'installing') {
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
const isRiskScoreAvailable =
|
||||
riskEngineStatus.data &&
|
||||
riskEngineStatus.data.risk_engine_status !== RiskEngineStatusEnum.NOT_INSTALLED;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" data-test-subj="entityStorePanelsGroup">
|
||||
{entityStore.status === 'enabled' && isRiskScoreAvailable && (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.user} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.host} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EntitiesList />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
{entityStore.status === 'enabled' && !isRiskScoreAvailable && (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EnableEntityStore
|
||||
onEnable={() => setModalState({ visible: true })}
|
||||
loadingRiskEngine={riskEngineInitializing}
|
||||
enablements="riskScore"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EntitiesList />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{entityStore.status === 'not_installed' && !isRiskScoreAvailable && (
|
||||
// TODO: Move modal inside EnableEntityStore component, eliminating the onEnable prop in favour of forwarding the riskScoreEnabled status
|
||||
<EnableEntityStore
|
||||
enablements="both"
|
||||
onEnable={() => setModalState({ visible: true })}
|
||||
loadingRiskEngine={riskEngineInitializing}
|
||||
/>
|
||||
)}
|
||||
|
||||
{entityStore.status === 'not_installed' && isRiskScoreAvailable && (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EnableEntityStore
|
||||
enablements="store"
|
||||
onEnable={() =>
|
||||
setModalState({
|
||||
visible: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.user} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.host} />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EntityStoreEnablementModal
|
||||
visible={modal.visible}
|
||||
toggle={(visible) => setModalState({ visible })}
|
||||
enableStore={enableEntityStore}
|
||||
riskScore={{ disabled: isRiskScoreAvailable, checked: !isRiskScoreAvailable }}
|
||||
entityStore={{
|
||||
disabled: entityStore.status === 'enabled',
|
||||
checked: entityStore.status !== 'enabled',
|
||||
}}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
interface EnableEntityStoreProps {
|
||||
onEnable: () => void;
|
||||
enablements: 'store' | 'riskScore' | 'both';
|
||||
loadingRiskEngine?: boolean;
|
||||
}
|
||||
|
||||
export const EnableEntityStore: React.FC<EnableEntityStoreProps> = ({
|
||||
onEnable,
|
||||
enablements,
|
||||
loadingRiskEngine,
|
||||
}) => {
|
||||
const title =
|
||||
enablements === 'store'
|
||||
? ENABLE_ENTITY_STORE_TITLE
|
||||
: enablements === 'riskScore'
|
||||
? ENABLE_RISK_SCORE_TITLE
|
||||
: ENABLE_ALL_TITLE;
|
||||
|
||||
const body =
|
||||
enablements === 'store'
|
||||
? ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY
|
||||
: enablements === 'riskScore'
|
||||
? ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY
|
||||
: ENABLEMENT_DESCRIPTION_BOTH;
|
||||
|
||||
if (loadingRiskEngine) {
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
<EuiEmptyPrompt
|
||||
icon={<EuiLoadingLogo logo="logoElastic" size="xl" />}
|
||||
title={<h2>{ENABLEMENT_INITIALIZING_RISK_ENGINE}</h2>}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
css={{ minWidth: '100%' }}
|
||||
hasBorder
|
||||
layout="horizontal"
|
||||
className="eui-fullWidth"
|
||||
title={<h2>{title}</h2>}
|
||||
body={<p>{body}</p>}
|
||||
actions={
|
||||
<EuiToolTip content={title}>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onEnable}
|
||||
data-test-subj={`enable_entity_store_btn`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStore.enablement.enableButton"
|
||||
defaultMessage="Enable"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
}
|
||||
icon={<EuiImage size="l" hasShadow src={dashboardEnableImg} alt={title} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EntityStoreDashboardPanels = React.memo(EntityStoreDashboardPanelsComponent);
|
||||
EntityStoreDashboardPanels.displayName = 'EntityStoreDashboardPanels';
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSwitch,
|
||||
EuiModalFooter,
|
||||
EuiButton,
|
||||
EuiHorizontalRule,
|
||||
EuiText,
|
||||
EuiButtonEmpty,
|
||||
EuiBetaBadge,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { TECHNICAL_PREVIEW, TECHNICAL_PREVIEW_TOOLTIP } from '../../../../common/translations';
|
||||
import {
|
||||
ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY,
|
||||
ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY,
|
||||
} from '../translations';
|
||||
|
||||
export interface Enablements {
|
||||
riskScore: boolean;
|
||||
entityStore: boolean;
|
||||
}
|
||||
|
||||
interface EntityStoreEnablementModalProps {
|
||||
visible: boolean;
|
||||
toggle: (visible: boolean) => void;
|
||||
enableStore: (enablements: Enablements) => () => void;
|
||||
riskScore: {
|
||||
disabled?: boolean;
|
||||
checked?: boolean;
|
||||
};
|
||||
entityStore: {
|
||||
disabled?: boolean;
|
||||
checked?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const EntityStoreEnablementModal: React.FC<EntityStoreEnablementModalProps> = ({
|
||||
visible,
|
||||
toggle,
|
||||
enableStore,
|
||||
riskScore,
|
||||
entityStore,
|
||||
}) => {
|
||||
const [enablements, setEnablements] = useState({
|
||||
riskScore: !!riskScore.checked,
|
||||
entityStore: !!entityStore.checked,
|
||||
});
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiModal onClose={() => toggle(false)}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.enablements.modal.title"
|
||||
defaultMessage="Additional charges may apply"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.enablements.modal.description"
|
||||
defaultMessage="Please be aware that activating these features may incur additional charges depending on your subscription plan. Review your plan details carefully to avoid unexpected costs before proceeding."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.enablements.modal.risk"
|
||||
defaultMessage="Risk Score"
|
||||
/>
|
||||
}
|
||||
checked={enablements.riskScore}
|
||||
disabled={riskScore.disabled || false}
|
||||
onChange={() => setEnablements((prev) => ({ ...prev, riskScore: !prev.riskScore }))}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>{ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexStart">
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.enablements.modal.store"
|
||||
defaultMessage="Entity Store"
|
||||
/>
|
||||
}
|
||||
checked={enablements.entityStore}
|
||||
disabled={entityStore.disabled || false}
|
||||
onChange={() =>
|
||||
setEnablements((prev) => ({ ...prev, entityStore: !prev.entityStore }))
|
||||
}
|
||||
/>
|
||||
<EuiToolTip content={TECHNICAL_PREVIEW_TOOLTIP}>
|
||||
<EuiBetaBadge label={TECHNICAL_PREVIEW} />
|
||||
</EuiToolTip>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>{ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={() => toggle(false)}>{'Cancel'}</EuiButtonEmpty>
|
||||
<EuiButton onClick={enableStore(enablements)} fill>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.enablements.modal.enable"
|
||||
defaultMessage="Enable"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -18,7 +18,7 @@ export enum EntitySource {
|
|||
CSV_UPLOAD = 'CSV upload',
|
||||
EVENTS = 'Events',
|
||||
}
|
||||
|
||||
// TODO Fix the Entity Source field before using it
|
||||
export const EntitySourceFilter: React.FC<SourceFilterProps> = ({ selectedItems, onChange }) => {
|
||||
return (
|
||||
<MultiselectFilter<EntitySource>
|
||||
|
|
|
@ -22,7 +22,6 @@ import type { Criteria } from '../../../explore/components/paginated_table';
|
|||
import { PaginatedTable } from '../../../explore/components/paginated_table';
|
||||
import { SeverityFilter } from '../severity/severity_filter';
|
||||
import type { EntitySource } from './components/entity_source_filter';
|
||||
import { EntitySourceFilter } from './components/entity_source_filter';
|
||||
import { useEntitiesListFilters } from './hooks/use_entities_list_filters';
|
||||
import { AssetCriticalityFilter } from '../asset_criticality/asset_criticality_filter';
|
||||
import { useEntitiesListQuery } from './hooks/use_entities_list_query';
|
||||
|
@ -41,7 +40,7 @@ export const EntitiesList: React.FC = () => {
|
|||
|
||||
const [selectedSeverities, setSelectedSeverities] = useState<RiskSeverity[]>([]);
|
||||
const [selectedCriticalities, setSelectedCriticalities] = useState<CriticalityLevels[]>([]);
|
||||
const [selectedSources, setSelectedSources] = useState<EntitySource[]>([]);
|
||||
const [selectedSources, _] = useState<EntitySource[]>([]);
|
||||
|
||||
const filter = useEntitiesListFilters({
|
||||
selectedSeverities,
|
||||
|
@ -148,7 +147,6 @@ export const EntitiesList: React.FC = () => {
|
|||
selectedItems={selectedCriticalities}
|
||||
onChange={setSelectedCriticalities}
|
||||
/>
|
||||
<EntitySourceFilter selectedItems={selectedSources} onChange={setSelectedSources} />
|
||||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -20,6 +20,7 @@ import type { Entity } from '../../../../../common/api/entity_analytics/entity_s
|
|||
import type { CriticalityLevels } from '../../../../../common/constants';
|
||||
import { ENTITIES_LIST_TABLE_ID } from '../constants';
|
||||
import { isUserEntity } from '../helpers';
|
||||
import { CRITICALITY_LEVEL_TITLE } from '../../asset_criticality/translations';
|
||||
|
||||
export type EntitiesListColumns = [
|
||||
Columns<Entity>,
|
||||
|
@ -86,6 +87,7 @@ export const useEntitiesListColumns = (): EntitiesListColumns => {
|
|||
/>
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: { lines: 2 },
|
||||
render: (_: string, record: Entity) => {
|
||||
return (
|
||||
<span>
|
||||
|
@ -94,7 +96,7 @@ export const useEntitiesListColumns = (): EntitiesListColumns => {
|
|||
</span>
|
||||
);
|
||||
},
|
||||
width: '30%',
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
field: 'entity.source',
|
||||
|
@ -104,7 +106,8 @@ export const useEntitiesListColumns = (): EntitiesListColumns => {
|
|||
defaultMessage="Source"
|
||||
/>
|
||||
),
|
||||
width: '10%',
|
||||
width: '25%',
|
||||
truncateText: { lines: 2 },
|
||||
render: (source: string | undefined) => {
|
||||
if (source != null) {
|
||||
return <span>{source}</span>;
|
||||
|
@ -124,7 +127,7 @@ export const useEntitiesListColumns = (): EntitiesListColumns => {
|
|||
width: '10%',
|
||||
render: (criticality: CriticalityLevels) => {
|
||||
if (criticality != null) {
|
||||
return criticality;
|
||||
return <span>{CRITICALITY_LEVEL_TITLE[criticality]}</span>;
|
||||
}
|
||||
|
||||
return getEmptyTagValue();
|
||||
|
@ -173,7 +176,7 @@ export const useEntitiesListColumns = (): EntitiesListColumns => {
|
|||
},
|
||||
},
|
||||
{
|
||||
field: 'entity.lastSeenTimestamp',
|
||||
field: '@timestamp',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStore.entitiesList.lastUpdateColumn.title"
|
||||
|
@ -184,7 +187,7 @@ export const useEntitiesListColumns = (): EntitiesListColumns => {
|
|||
render: (lastUpdate: string) => {
|
||||
return <FormattedRelativePreferenceDate value={lastUpdate} />;
|
||||
},
|
||||
width: '25%',
|
||||
width: '15%',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('useEntitiesListFilters', () => {
|
|||
mockUseGlobalFilterQuery.mockReturnValue({ filterQuery: null });
|
||||
});
|
||||
|
||||
it('should return empty array when no filters are selected', () => {
|
||||
it('should return empty filter when no filters are selected', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEntitiesListFilters({
|
||||
selectedSeverities: [],
|
||||
|
@ -49,13 +49,6 @@ describe('useEntitiesListFilters', () => {
|
|||
should: [
|
||||
{ term: { 'host.risk.calculated_level': RiskSeverity.Low } },
|
||||
{ term: { 'user.risk.calculated_level': RiskSeverity.Low } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ term: { 'host.risk.calculated_level': RiskSeverity.High } },
|
||||
{ term: { 'user.risk.calculated_level': RiskSeverity.High } },
|
||||
],
|
||||
|
@ -77,8 +70,23 @@ describe('useEntitiesListFilters', () => {
|
|||
);
|
||||
|
||||
const expectedFilters: QueryDslQueryContainer[] = [
|
||||
{ term: { 'asset.criticality': CriticalityLevels.EXTREME_IMPACT } },
|
||||
{ term: { 'asset.criticality': CriticalityLevels.MEDIUM_IMPACT } },
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
'asset.criticality': CriticalityLevels.EXTREME_IMPACT,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'asset.criticality': CriticalityLevels.MEDIUM_IMPACT,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(result.current).toEqual(expectedFilters);
|
||||
|
@ -138,7 +146,12 @@ describe('useEntitiesListFilters', () => {
|
|||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{ term: { 'asset.criticality': CriticalityLevels.HIGH_IMPACT } },
|
||||
{
|
||||
bool: {
|
||||
should: [{ term: { 'asset.criticality': CriticalityLevels.HIGH_IMPACT } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{ term: { 'entity.source': EntitySource.CSV_UPLOAD } },
|
||||
globalQuery,
|
||||
];
|
||||
|
|
|
@ -26,11 +26,20 @@ export const useEntitiesListFilters = ({
|
|||
const { filterQuery: globalQuery } = useGlobalFilterQuery();
|
||||
|
||||
return useMemo(() => {
|
||||
const criticalityFilter: QueryDslQueryContainer[] = selectedCriticalities.map((value) => ({
|
||||
term: {
|
||||
'asset.criticality': value,
|
||||
},
|
||||
}));
|
||||
const criticalityFilter: QueryDslQueryContainer[] = selectedCriticalities.length
|
||||
? [
|
||||
{
|
||||
bool: {
|
||||
should: selectedCriticalities.map((value) => ({
|
||||
term: {
|
||||
'asset.criticality': value,
|
||||
},
|
||||
})),
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const sourceFilter: QueryDslQueryContainer[] = selectedSources.map((value) => ({
|
||||
term: {
|
||||
|
@ -38,23 +47,27 @@ export const useEntitiesListFilters = ({
|
|||
},
|
||||
}));
|
||||
|
||||
const severityFilter: QueryDslQueryContainer[] = selectedSeverities.map((value) => ({
|
||||
bool: {
|
||||
should: [
|
||||
const severityFilter: QueryDslQueryContainer[] = selectedSeverities.length
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
'host.risk.calculated_level': value,
|
||||
bool: {
|
||||
should: selectedSeverities.flatMap((value) => [
|
||||
{
|
||||
term: {
|
||||
'host.risk.calculated_level': value,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'user.risk.calculated_level': value,
|
||||
},
|
||||
},
|
||||
]),
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'user.risk.calculated_level': value,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
}));
|
||||
]
|
||||
: [];
|
||||
|
||||
const filterList: QueryDslQueryContainer[] = [
|
||||
...severityFilter,
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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_STATUS = 'ENTITY_STORE_ENGINE_STATUS';
|
||||
|
||||
interface Options {
|
||||
disabled?: boolean;
|
||||
polling?: UseQueryOptions<ListEntityEnginesResponse>['refetchInterval'];
|
||||
}
|
||||
|
||||
export const useEntityEngineStatus = (opts: Options = {}) => {
|
||||
// QUESTION: Maybe we should have an `EnablementStatus` API route for this?
|
||||
const { listEntityEngines } = useEntityStoreRoutes();
|
||||
|
||||
const { isLoading, data } = useQuery<ListEntityEnginesResponse>({
|
||||
queryKey: [ENTITY_STORE_ENGINE_STATUS],
|
||||
queryFn: () => listEntityEngines(),
|
||||
refetchInterval: opts.polling,
|
||||
enabled: !opts.disabled,
|
||||
});
|
||||
|
||||
const status = (() => {
|
||||
if (data?.count === 0) {
|
||||
return 'not_installed';
|
||||
}
|
||||
|
||||
if (data?.engines?.every((engine) => engine.status === 'stopped')) {
|
||||
return 'stopped';
|
||||
}
|
||||
|
||||
if (data?.engines?.some((engine) => engine.status === 'installing')) {
|
||||
return 'installing';
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return 'loading';
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
return 'enabled';
|
||||
})();
|
||||
|
||||
return {
|
||||
status,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 { UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type {
|
||||
DeleteEntityEngineResponse,
|
||||
InitEntityEngineResponse,
|
||||
StopEntityEngineResponse,
|
||||
} from '../../../../../common/api/entity_analytics';
|
||||
import { useEntityStoreRoutes } from '../../../api/entity_store';
|
||||
import { ENTITY_STORE_ENGINE_STATUS, useEntityEngineStatus } from './use_entity_engine_status';
|
||||
|
||||
const ENTITY_STORE_ENABLEMENT_INIT = 'ENTITY_STORE_ENABLEMENT_INIT';
|
||||
|
||||
export const useEntityStoreEnablement = () => {
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
||||
const { initEntityStore } = useEntityStoreRoutes();
|
||||
const { refetch: initialize } = useQuery({
|
||||
queryKey: [ENTITY_STORE_ENABLEMENT_INIT],
|
||||
queryFn: () => Promise.all([initEntityStore('user'), initEntityStore('host')]),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const enable = useCallback(() => {
|
||||
initialize().then(() => setPolling(true));
|
||||
}, [initialize]);
|
||||
|
||||
return { enable };
|
||||
};
|
||||
|
||||
export const INIT_ENTITY_ENGINE_STATUS_KEY = ['POST', 'INIT_ENTITY_ENGINE'];
|
||||
|
||||
export const useInvalidateEntityEngineStatusQuery = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useCallback(() => {
|
||||
queryClient.invalidateQueries([ENTITY_STORE_ENGINE_STATUS], {
|
||||
refetchType: 'active',
|
||||
});
|
||||
}, [queryClient]);
|
||||
};
|
||||
|
||||
export const useInitEntityEngineMutation = (options?: UseMutationOptions<{}>) => {
|
||||
const invalidateEntityEngineStatusQuery = useInvalidateEntityEngineStatusQuery();
|
||||
const { initEntityStore } = useEntityStoreRoutes();
|
||||
return useMutation<InitEntityEngineResponse[]>(
|
||||
() => Promise.all([initEntityStore('user'), initEntityStore('host')]),
|
||||
{
|
||||
...options,
|
||||
mutationKey: INIT_ENTITY_ENGINE_STATUS_KEY,
|
||||
onSettled: (...args) => {
|
||||
invalidateEntityEngineStatusQuery();
|
||||
|
||||
if (options?.onSettled) {
|
||||
options.onSettled(...args);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const STOP_ENTITY_ENGINE_STATUS_KEY = ['POST', 'STOP_ENTITY_ENGINE'];
|
||||
|
||||
export const useStopEntityEngineMutation = (options?: UseMutationOptions<{}>) => {
|
||||
const invalidateEntityEngineStatusQuery = useInvalidateEntityEngineStatusQuery();
|
||||
const { stopEntityStore } = useEntityStoreRoutes();
|
||||
return useMutation<StopEntityEngineResponse[]>(
|
||||
() => Promise.all([stopEntityStore('user'), stopEntityStore('host')]),
|
||||
{
|
||||
...options,
|
||||
mutationKey: STOP_ENTITY_ENGINE_STATUS_KEY,
|
||||
onSettled: (...args) => {
|
||||
invalidateEntityEngineStatusQuery();
|
||||
|
||||
if (options?.onSettled) {
|
||||
options.onSettled(...args);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const DELETE_ENTITY_ENGINE_STATUS_KEY = ['POST', 'STOP_ENTITY_ENGINE'];
|
||||
|
||||
export const useDeleteEntityEngineMutation = (options?: UseMutationOptions<{}>) => {
|
||||
const invalidateEntityEngineStatusQuery = useInvalidateEntityEngineStatusQuery();
|
||||
const { deleteEntityEngine } = useEntityStoreRoutes();
|
||||
return useMutation<DeleteEntityEngineResponse[]>(
|
||||
() => Promise.all([deleteEntityEngine('user'), deleteEntityEngine('host')]),
|
||||
{
|
||||
...options,
|
||||
mutationKey: DELETE_ENTITY_ENGINE_STATUS_KEY,
|
||||
onSettled: (...args) => {
|
||||
invalidateEntityEngineStatusQuery();
|
||||
|
||||
if (options?.onSettled) {
|
||||
options.onSettled(...args);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ENABLE_ENTITY_STORE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.entityStore.enablement.title.store',
|
||||
{
|
||||
defaultMessage: 'Enable entity store',
|
||||
}
|
||||
);
|
||||
export const ENABLE_RISK_SCORE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.entityStore.enablement.title.risk',
|
||||
{
|
||||
defaultMessage: 'Enable entity risk score',
|
||||
}
|
||||
);
|
||||
export const ENABLE_ALL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.entityStore.enablement.title.both',
|
||||
{
|
||||
defaultMessage: 'Enable entity store and risk score',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENABLEMENT_INITIALIZING_RISK_ENGINE = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.entityStore.enablement.initializing.risk',
|
||||
{
|
||||
defaultMessage: 'Initializing risk engine',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENABLEMENT_INITIALIZING_ENTITY_STORE = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.entityStore.enablement.initializing.store',
|
||||
{
|
||||
defaultMessage: 'Initializing entity store',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENABLEMENT_DESCRIPTION_RISK_ENGINE_ONLY = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.entityStore.enablement.description.risk',
|
||||
{
|
||||
defaultMessage:
|
||||
'Provides real-time visibility into user activity, helping you identify and mitigate potential security risks.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENABLEMENT_DESCRIPTION_ENTITY_STORE_ONLY = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.entityStore.enablement.description.store',
|
||||
{
|
||||
defaultMessage: "Allows comprehensive monitoring of your system's hosts and users.",
|
||||
}
|
||||
);
|
||||
|
||||
export const ENABLEMENT_DESCRIPTION_BOTH = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.entityStore.enablement.description.both',
|
||||
{
|
||||
defaultMessage:
|
||||
'Your entity store is currently empty. Add information about your entities directly from your logs, or import them using a text file.',
|
||||
}
|
||||
);
|
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
|
@ -1,186 +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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPageHeader,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiEmptyPrompt,
|
||||
EuiCallOut,
|
||||
EuiCode,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { ASSET_CRITICALITY_INDEX_PATTERN } from '../../../common/entity_analytics/asset_criticality';
|
||||
import { useUiSetting$, useKibana } from '../../common/lib/kibana';
|
||||
import { ENABLE_ASSET_CRITICALITY_SETTING } from '../../../common/constants';
|
||||
import { AssetCriticalityFileUploader } from '../components/asset_criticality_file_uploader/asset_criticality_file_uploader';
|
||||
import { useAssetCriticalityPrivileges } from '../components/asset_criticality/use_asset_criticality';
|
||||
import { useHasSecurityCapability } from '../../helper_hooks';
|
||||
|
||||
export const AssetCriticalityUploadPage = () => {
|
||||
const { docLinks } = useKibana().services;
|
||||
const entityAnalyticsLinks = docLinks.links.securitySolution.entityAnalytics;
|
||||
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
|
||||
const [isAssetCriticalityEnabled] = useUiSetting$<boolean>(ENABLE_ASSET_CRITICALITY_SETTING);
|
||||
const {
|
||||
data: privileges,
|
||||
error: privilegesError,
|
||||
isLoading,
|
||||
} = useAssetCriticalityPrivileges('AssetCriticalityUploadPage');
|
||||
const hasWritePermissions = privileges?.has_write_permissions;
|
||||
|
||||
if (isLoading) {
|
||||
// Wait for permission before rendering content to avoid flickering
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!hasEntityAnalyticsCapability ||
|
||||
!isAssetCriticalityEnabled ||
|
||||
privilegesError?.body.status_code === 403
|
||||
) {
|
||||
const errorMessage = privilegesError?.body.message ?? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledMessage"
|
||||
defaultMessage='Please enable "{ENABLE_ASSET_CRITICALITY_SETTING}" on advanced settings to access the page.'
|
||||
values={{
|
||||
ENABLE_ASSET_CRITICALITY_SETTING,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledTitle"
|
||||
defaultMessage="This page is disabled"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={<p>{errorMessage}</p>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasWritePermissions) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.noPermissionTitle"
|
||||
defaultMessage="Insufficient index privileges to access this page"
|
||||
/>
|
||||
}
|
||||
color="primary"
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.missingPermissionsCallout.description"
|
||||
defaultMessage="Write permission is required for the {index} index pattern in order to access this page. Contact your administrator for further assistance."
|
||||
values={{
|
||||
index: <EuiCode>{ASSET_CRITICALITY_INDEX_PATTERN}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPageHeader
|
||||
data-test-subj="assetCriticalityUploadPage"
|
||||
pageTitle={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.title"
|
||||
defaultMessage="Asset criticality"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<EuiHorizontalRule />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup gutterSize="xl">
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiTitle size="s">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.subTitle"
|
||||
defaultMessage="Import your asset criticality data"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.description"
|
||||
defaultMessage="Bulk assign asset criticality by importing a CSV, TXT, or TSV file exported from your asset management tools. This ensures data accuracy and reduces manual input errors."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<AssetCriticalityFileUploader />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiPanel hasBorder={true} paddingSize="l" grow={false}>
|
||||
<EuiIcon type="questionInCircle" size="xl" />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="xxs">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.information.title"
|
||||
defaultMessage="What is asset criticality?"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.information.description"
|
||||
defaultMessage="Asset criticality allows you to classify entities based on their importance and impact on business operations. Use asset criticality to guide prioritization for alert triaging, threat-hunting, and investigation activities."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiHorizontalRule />
|
||||
<EuiTitle size="xxs">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.information.usefulLinks"
|
||||
defaultMessage="Useful links"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
rel="noopener nofollow noreferrer"
|
||||
href={entityAnalyticsLinks.assetCriticality}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.documentationLink"
|
||||
defaultMessage="Asset criticality documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AssetCriticalityUploadPage.displayName = 'AssetCriticalityUploadPage';
|
|
@ -23,16 +23,16 @@ import { RiskScoreUpdatePanel } from '../components/risk_score_update_panel';
|
|||
import { useHasSecurityCapability } from '../../helper_hooks';
|
||||
import { EntityAnalyticsHeader } from '../components/entity_analytics_header';
|
||||
import { EntityAnalyticsAnomalies } from '../components/entity_analytics_anomalies';
|
||||
|
||||
import { EntityStoreDashboardPanels } from '../components/entity_store/components/dashboard_panels';
|
||||
import { EntityAnalyticsRiskScores } from '../components/entity_analytics_risk_score';
|
||||
import { EntitiesList } from '../components/entity_store/entities_list';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
|
||||
|
||||
const EntityAnalyticsComponent = () => {
|
||||
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
|
||||
const { indicesExist, loading: isSourcererLoading, sourcererDataView } = useSourcererDataView();
|
||||
const isRiskScoreModuleLicenseAvailable = useHasSecurityCapability('entity-analytics');
|
||||
|
||||
const isEntityStoreDisabled = useIsExperimentalFeatureEnabled('entityStoreDisabled');
|
||||
const isEntityStoreFeatureFlagDisabled = useIsExperimentalFeatureEnabled('entityStoreDisabled');
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -59,23 +59,25 @@ const EntityAnalyticsComponent = () => {
|
|||
<EntityAnalyticsHeader />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.host} />
|
||||
</EuiFlexItem>
|
||||
{!isEntityStoreFeatureFlagDisabled ? (
|
||||
<EuiFlexItem>
|
||||
<EntityStoreDashboardPanels />
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.host} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.user} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.user} />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiFlexItem>
|
||||
<EntityAnalyticsAnomalies />
|
||||
</EuiFlexItem>
|
||||
|
||||
{!isEntityStoreDisabled ? (
|
||||
<EuiFlexItem>
|
||||
<EntitiesList />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</SecuritySolutionPageWrapper>
|
||||
|
|
|
@ -0,0 +1,456 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiConfirmModal,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPageHeader,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiCallOut,
|
||||
EuiCode,
|
||||
EuiSwitch,
|
||||
EuiHealth,
|
||||
EuiButton,
|
||||
EuiLoadingSpinner,
|
||||
EuiToolTip,
|
||||
EuiBetaBadge,
|
||||
} 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 { useUiSetting$, useKibana } from '../../common/lib/kibana';
|
||||
import { ENABLE_ASSET_CRITICALITY_SETTING } from '../../../common/constants';
|
||||
import { AssetCriticalityFileUploader } from '../components/asset_criticality_file_uploader/asset_criticality_file_uploader';
|
||||
import { useAssetCriticalityPrivileges } from '../components/asset_criticality/use_asset_criticality';
|
||||
import { useHasSecurityCapability } from '../../helper_hooks';
|
||||
import {
|
||||
useDeleteEntityEngineMutation,
|
||||
useInitEntityEngineMutation,
|
||||
useStopEntityEngineMutation,
|
||||
} from '../components/entity_store/hooks/use_entity_store';
|
||||
import { TECHNICAL_PREVIEW, TECHNICAL_PREVIEW_TOOLTIP } from '../../common/translations';
|
||||
|
||||
const entityStoreEnabledStatuses = ['enabled'];
|
||||
const switchDisabledStatuses = ['error', 'loading', 'installing'];
|
||||
const entityStoreInstallingStatuses = ['installing', 'loading'];
|
||||
|
||||
export const EntityStoreManagementPage = () => {
|
||||
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
|
||||
const isEntityStoreFeatureFlagDisabled = useIsExperimentalFeatureEnabled('entityStoreDisabled');
|
||||
const [isAssetCriticalityEnabled] = useUiSetting$<boolean>(ENABLE_ASSET_CRITICALITY_SETTING);
|
||||
const {
|
||||
data: assetCriticalityPrivileges,
|
||||
error: assetCriticalityPrivilegesError,
|
||||
isLoading: assetCriticalityIsLoading,
|
||||
} = useAssetCriticalityPrivileges('AssetCriticalityUploadPage');
|
||||
const hasAssetCriticalityWritePermissions = assetCriticalityPrivileges?.has_write_permissions;
|
||||
|
||||
const [polling, setPolling] = useState(false);
|
||||
const entityStoreStatus = useEntityEngineStatus({
|
||||
disabled: false,
|
||||
polling: !polling
|
||||
? undefined
|
||||
: (data) => {
|
||||
const shouldStopPolling =
|
||||
data?.engines &&
|
||||
data.engines.length > 0 &&
|
||||
data.engines.every((engine) => engine.status === 'started');
|
||||
|
||||
if (shouldStopPolling) {
|
||||
setPolling(false);
|
||||
return false;
|
||||
}
|
||||
return 1000;
|
||||
},
|
||||
});
|
||||
const initEntityEngineMutation = useInitEntityEngineMutation();
|
||||
const stopEntityEngineMutation = useStopEntityEngineMutation();
|
||||
const deleteEntityEngineMutation = useDeleteEntityEngineMutation({
|
||||
onSuccess: () => {
|
||||
closeClearModal();
|
||||
},
|
||||
});
|
||||
|
||||
const [isClearModalVisible, setIsClearModalVisible] = useState(false);
|
||||
const closeClearModal = useCallback(() => setIsClearModalVisible(false), []);
|
||||
const showClearModal = useCallback(() => setIsClearModalVisible(true), []);
|
||||
|
||||
const onSwitchClick = useCallback(() => {
|
||||
if (switchDisabledStatuses.includes(entityStoreStatus.status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entityStoreEnabledStatuses.includes(entityStoreStatus.status)) {
|
||||
stopEntityEngineMutation.mutate();
|
||||
} else {
|
||||
setPolling(true);
|
||||
initEntityEngineMutation.mutate();
|
||||
}
|
||||
}, [initEntityEngineMutation, stopEntityEngineMutation, entityStoreStatus]);
|
||||
|
||||
if (assetCriticalityIsLoading) {
|
||||
// Wait for permission before rendering content to avoid flickering
|
||||
return null;
|
||||
}
|
||||
|
||||
const AssetCriticalityIssueCallout: React.FC = () => {
|
||||
const errorMessage = assetCriticalityPrivilegesError?.body.message ?? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledMessage"
|
||||
defaultMessage='Please enable "{ENABLE_ASSET_CRITICALITY_SETTING}" in advanced settings to access this functionality.'
|
||||
values={{
|
||||
ENABLE_ASSET_CRITICALITY_SETTING,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unavailable"
|
||||
defaultMessage="Asset criticality CSV file upload functionality unavailable."
|
||||
/>
|
||||
}
|
||||
color="primary"
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<EuiText size="s">{errorMessage}</EuiText>
|
||||
</EuiCallOut>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ClearEntityDataPanel: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<EuiPanel
|
||||
paddingSize="l"
|
||||
grow={false}
|
||||
color="subdued"
|
||||
borderRadius="none"
|
||||
hasShadow={false}
|
||||
>
|
||||
<EuiText size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntityData"
|
||||
defaultMessage="Clear entity data"
|
||||
/>
|
||||
</h3>
|
||||
<EuiSpacer size="s" />
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntityData"
|
||||
defaultMessage={`Remove all extracted entity data from the store. This action will
|
||||
permanently delete persisted user and host records, and data will no longer be available for analysis.
|
||||
Proceed with caution, as this cannot be undone. Note that this operation will not delete source data,
|
||||
Entity risk scores, or Asset Criticality assignments.`}
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButton
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={() => {
|
||||
showClearModal();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clear"
|
||||
defaultMessage="Clear"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiPanel>
|
||||
{isClearModalVisible && (
|
||||
<EuiConfirmModal
|
||||
isLoading={deleteEntityEngineMutation.isLoading}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.title"
|
||||
defaultMessage="Clear Entity data?"
|
||||
/>
|
||||
}
|
||||
onCancel={closeClearModal}
|
||||
onConfirm={() => {
|
||||
deleteEntityEngineMutation.mutate();
|
||||
}}
|
||||
cancelButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.close"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
}
|
||||
confirmButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.clearAllEntities"
|
||||
defaultMessage="Clear All Entities"
|
||||
/>
|
||||
}
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton="confirm"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearConfirmation"
|
||||
defaultMessage={
|
||||
'This will delete all Security Entity store records. Source data, Entity risk scores, and Asset criticality assignments are unaffected by this action. This operation cannot be undone.'
|
||||
}
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FileUploadSection: React.FC = () => {
|
||||
if (
|
||||
!hasEntityAnalyticsCapability ||
|
||||
!isAssetCriticalityEnabled ||
|
||||
assetCriticalityPrivilegesError?.body.status_code === 403
|
||||
) {
|
||||
return <AssetCriticalityIssueCallout />;
|
||||
}
|
||||
if (!hasAssetCriticalityWritePermissions) {
|
||||
return <InsufficientAssetCriticalityPrivilegesCallout />;
|
||||
}
|
||||
return (
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiTitle size="s">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.subTitle"
|
||||
defaultMessage="Import entities using a text file"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.description"
|
||||
defaultMessage="Bulk assign asset criticality by importing a CSV, TXT, or TSV file exported from your asset management tools. This ensures data accuracy and reduces manual input errors."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<AssetCriticalityFileUploader />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
const canDeleteEntityEngine = !['not_installed', 'loading', 'installing'].includes(
|
||||
entityStoreStatus.status
|
||||
);
|
||||
|
||||
const isMutationLoading =
|
||||
initEntityEngineMutation.isLoading ||
|
||||
stopEntityEngineMutation.isLoading ||
|
||||
deleteEntityEngineMutation.isLoading;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPageHeader
|
||||
data-test-subj="entityStoreManagementPage"
|
||||
pageTitle={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.title"
|
||||
defaultMessage="Entity Store"
|
||||
/>{' '}
|
||||
<EuiToolTip content={TECHNICAL_PREVIEW_TOOLTIP}>
|
||||
<EuiBetaBadge label={TECHNICAL_PREVIEW} />
|
||||
</EuiToolTip>
|
||||
</>
|
||||
}
|
||||
alignItems="center"
|
||||
rightSideItems={
|
||||
!isEntityStoreFeatureFlagDisabled
|
||||
? [
|
||||
<EnablementButton
|
||||
isLoading={
|
||||
isMutationLoading ||
|
||||
entityStoreInstallingStatuses.includes(entityStoreStatus.status)
|
||||
}
|
||||
isDisabled={
|
||||
isMutationLoading || switchDisabledStatuses.includes(entityStoreStatus.status)
|
||||
}
|
||||
onSwitch={onSwitchClick}
|
||||
status={entityStoreStatus.status}
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.subTitle"
|
||||
defaultMessage="Allows comprehensive monitoring of your system's hosts and users."
|
||||
/>
|
||||
</EuiText>
|
||||
{isEntityStoreFeatureFlagDisabled && <EntityStoreFeatureFlagNotAvailableCallout />}
|
||||
<EuiHorizontalRule />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup gutterSize="xl">
|
||||
<FileUploadSection />
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiFlexGroup direction="column">
|
||||
<WhatIsAssetCriticalityPanel />
|
||||
{!isEntityStoreFeatureFlagDisabled && canDeleteEntityEngine && <ClearEntityDataPanel />}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
EntityStoreManagementPage.displayName = 'EntityStoreManagementPage';
|
||||
|
||||
const WhatIsAssetCriticalityPanel: React.FC = () => {
|
||||
const { docLinks } = useKibana().services;
|
||||
const entityAnalyticsLinks = docLinks.links.securitySolution.entityAnalytics;
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={true} paddingSize="l" grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiIcon type="questionInCircle" size="xl" />
|
||||
<EuiTitle size="xxs">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.information.title"
|
||||
defaultMessage="What is asset criticality?"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.information.description"
|
||||
defaultMessage="Asset criticality allows you to classify entities based on their importance and impact on business operations. Use asset criticality to guide prioritization for alert triaging, threat-hunting, and investigation activities."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiHorizontalRule />
|
||||
<EuiTitle size="xxs">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.information.usefulLinks"
|
||||
defaultMessage="Useful links"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
rel="noopener nofollow noreferrer"
|
||||
href={entityAnalyticsLinks.assetCriticality}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.documentationLink"
|
||||
defaultMessage="Asset criticality documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
const EntityStoreFeatureFlagNotAvailableCallout: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.featureFlagDisabled"
|
||||
defaultMessage="Entity Store capabilities not available"
|
||||
/>
|
||||
}
|
||||
color="primary"
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.entityStoreManagementPage.featureFlagDisabledDescription"
|
||||
defaultMessage="The full capabilities of the Entity Store have been disabled in this environment. Contact your administrator for further assistance."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EntityStoreHealth: React.FC<{ currentEntityStoreStatus: string }> = ({
|
||||
currentEntityStoreStatus,
|
||||
}) => {
|
||||
return (
|
||||
<EuiHealth
|
||||
textSize="m"
|
||||
color={entityStoreEnabledStatuses.includes(currentEntityStoreStatus) ? 'success' : 'subdued'}
|
||||
>
|
||||
{entityStoreEnabledStatuses.includes(currentEntityStoreStatus) ? 'On' : 'Off'}
|
||||
</EuiHealth>
|
||||
);
|
||||
};
|
||||
|
||||
const EnablementButton: React.FC<{
|
||||
isLoading: boolean;
|
||||
isDisabled: boolean;
|
||||
status: string;
|
||||
onSwitch: () => void;
|
||||
}> = ({ isLoading, isDisabled, status, onSwitch }) => {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
{isLoading && (
|
||||
<EuiFlexItem>
|
||||
<EuiLoadingSpinner data-test-subj="entity-store-status-loading" size="m" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EntityStoreHealth currentEntityStoreStatus={status} />
|
||||
<EuiSwitch
|
||||
showLabel={false}
|
||||
label=""
|
||||
onChange={onSwitch}
|
||||
data-test-subj="entity-store-switch"
|
||||
checked={entityStoreEnabledStatuses.includes(status)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const InsufficientAssetCriticalityPrivilegesCallout: React.FC = () => {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.noPermissionTitle"
|
||||
defaultMessage="Insufficient index privileges to perform CSV upload"
|
||||
/>
|
||||
}
|
||||
color="primary"
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.missingPermissionsCallout.description"
|
||||
defaultMessage="Write permission is required for the {index} index pattern in order to access this functionality. Contact your administrator for further assistance."
|
||||
values={{
|
||||
index: <EuiCode>{ASSET_CRITICALITY_INDEX_PATTERN}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
|
@ -14,12 +14,13 @@ import { NotFoundPage } from '../app/404';
|
|||
|
||||
import {
|
||||
ENTITY_ANALYTICS_ASSET_CRITICALITY_PATH,
|
||||
ENTITY_ANALYTICS_ENTITY_STORE_MANAGEMENT_PATH,
|
||||
ENTITY_ANALYTICS_MANAGEMENT_PATH,
|
||||
SecurityPageName,
|
||||
} from '../../common/constants';
|
||||
import { EntityAnalyticsManagementPage } from './pages/entity_analytics_management_page';
|
||||
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
|
||||
import { AssetCriticalityUploadPage } from './pages/asset_criticality_upload_page';
|
||||
import { EntityStoreManagementPage } from './pages/entity_store_management_page';
|
||||
|
||||
const EntityAnalyticsManagementTelemetry = () => (
|
||||
<PluginTemplateWrapper>
|
||||
|
@ -47,7 +48,7 @@ EntityAnalyticsManagementContainer.displayName = 'EntityAnalyticsManagementConta
|
|||
const EntityAnalyticsAssetClassificationTelemetry = () => (
|
||||
<PluginTemplateWrapper>
|
||||
<TrackApplicationView viewId={SecurityPageName.entityAnalyticsAssetClassification}>
|
||||
<AssetCriticalityUploadPage />
|
||||
<EntityStoreManagementPage />
|
||||
<SpyRoute pageName={SecurityPageName.entityAnalyticsAssetClassification} />
|
||||
</TrackApplicationView>
|
||||
</PluginTemplateWrapper>
|
||||
|
@ -69,6 +70,30 @@ const EntityAnalyticsAssetClassificationContainer: React.FC = React.memo(() => {
|
|||
EntityAnalyticsAssetClassificationContainer.displayName =
|
||||
'EntityAnalyticsAssetClassificationContainer';
|
||||
|
||||
const EntityAnalyticsEntityStoreTelemetry = () => (
|
||||
<PluginTemplateWrapper>
|
||||
<TrackApplicationView viewId={SecurityPageName.entityAnalyticsEntityStoreManagement}>
|
||||
<EntityStoreManagementPage />
|
||||
<SpyRoute pageName={SecurityPageName.entityAnalyticsEntityStoreManagement} />
|
||||
</TrackApplicationView>
|
||||
</PluginTemplateWrapper>
|
||||
);
|
||||
|
||||
const EntityAnalyticsEntityStoreContainer: React.FC = React.memo(() => {
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
path={ENTITY_ANALYTICS_ENTITY_STORE_MANAGEMENT_PATH}
|
||||
exact
|
||||
component={EntityAnalyticsEntityStoreTelemetry}
|
||||
/>
|
||||
<Route component={NotFoundPage} />
|
||||
</Switch>
|
||||
);
|
||||
});
|
||||
|
||||
EntityAnalyticsEntityStoreContainer.displayName = 'EntityAnalyticsEntityStoreContainer';
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: ENTITY_ANALYTICS_MANAGEMENT_PATH,
|
||||
|
@ -78,4 +103,8 @@ export const routes = [
|
|||
path: ENTITY_ANALYTICS_ASSET_CRITICALITY_PATH,
|
||||
component: EntityAnalyticsAssetClassificationContainer,
|
||||
},
|
||||
{
|
||||
path: ENTITY_ANALYTICS_ENTITY_STORE_MANAGEMENT_PATH,
|
||||
component: EntityAnalyticsEntityStoreContainer,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -138,7 +138,7 @@ export interface Columns<T, U = T> {
|
|||
name: string | React.ReactNode;
|
||||
render?: (item: T, node: U) => React.ReactNode;
|
||||
sortable?: boolean | Func<T>;
|
||||
truncateText?: boolean;
|
||||
truncateText?: boolean | { lines: number };
|
||||
width?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,8 @@ import {
|
|||
} from '../../common/endpoint/service/authz';
|
||||
import {
|
||||
BLOCKLIST_PATH,
|
||||
ENABLE_ASSET_CRITICALITY_SETTING,
|
||||
ENDPOINTS_PATH,
|
||||
ENTITY_ANALYTICS_ASSET_CRITICALITY_PATH,
|
||||
ENTITY_ANALYTICS_ENTITY_STORE_MANAGEMENT_PATH,
|
||||
ENTITY_ANALYTICS_MANAGEMENT_PATH,
|
||||
EVENT_FILTERS_PATH,
|
||||
HOST_ISOLATION_EXCEPTIONS_PATH,
|
||||
|
@ -39,8 +38,8 @@ import {
|
|||
RESPONSE_ACTIONS_HISTORY,
|
||||
TRUSTED_APPLICATIONS,
|
||||
ENTITY_ANALYTICS_RISK_SCORE,
|
||||
ASSET_CRITICALITY,
|
||||
NOTES,
|
||||
ENTITY_STORE,
|
||||
} from '../app/translations';
|
||||
import { licenseService } from '../common/hooks/use_license';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
|
@ -64,7 +63,7 @@ const categories = [
|
|||
}),
|
||||
linkIds: [
|
||||
SecurityPageName.entityAnalyticsManagement,
|
||||
SecurityPageName.entityAnalyticsAssetClassification,
|
||||
SecurityPageName.entityAnalyticsEntityStoreManagement,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -196,20 +195,16 @@ export const links: LinkItem = {
|
|||
licenseType: 'platinum',
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.entityAnalyticsAssetClassification,
|
||||
title: ASSET_CRITICALITY,
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.appLinks.assetClassificationDescription',
|
||||
{
|
||||
defaultMessage: 'Represents the criticality of an asset to your business infrastructure.',
|
||||
}
|
||||
),
|
||||
id: SecurityPageName.entityAnalyticsEntityStoreManagement,
|
||||
title: ENTITY_STORE,
|
||||
description: i18n.translate('xpack.securitySolution.appLinks.entityStoreDescription', {
|
||||
defaultMessage: "Allows comprehensive monitoring of your system's hosts and users.",
|
||||
}),
|
||||
landingIcon: IconAssetCriticality,
|
||||
path: ENTITY_ANALYTICS_ASSET_CRITICALITY_PATH,
|
||||
path: ENTITY_ANALYTICS_ENTITY_STORE_MANAGEMENT_PATH,
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
capabilities: [`${SERVER_APP_ID}.entity-analytics`],
|
||||
uiSettingRequired: ENABLE_ASSET_CRITICALITY_SETTING,
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.responseActionsHistory,
|
||||
|
|
|
@ -5,12 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EngineStatus } from '../../../../common/api/entity_analytics/entity_store/common.gen';
|
||||
|
||||
/**
|
||||
* Default index pattern for entity store
|
||||
* This is the same as the default index pattern for the SIEM app but might diverge in the future
|
||||
*/
|
||||
import type { EngineStatus } from '../../../../common/api/entity_analytics';
|
||||
|
||||
export const DEFAULT_LOOKBACK_PERIOD = '24h';
|
||||
|
||||
|
@ -21,6 +16,7 @@ export const ENGINE_STATUS: Record<Uppercase<EngineStatus>, EngineStatus> = {
|
|||
STARTED: 'started',
|
||||
STOPPED: 'stopped',
|
||||
UPDATING: 'updating',
|
||||
ERROR: 'error',
|
||||
};
|
||||
|
||||
export const MAX_SEARCH_RESPONSE_SIZE = 10_000;
|
||||
|
|
|
@ -16,17 +16,15 @@ import type { SortOrder } from '@elastic/elasticsearch/lib/api/types';
|
|||
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
|
||||
import type { DataViewsService } from '@kbn/data-views-plugin/common';
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import type { EngineDataviewUpdateResult } from '../../../../common/api/entity_analytics/entity_store/engine/apply_dataview_indices.gen';
|
||||
import type { AppClient } from '../../..';
|
||||
import type { Entity } from '../../../../common/api/entity_analytics/entity_store/entities/common.gen';
|
||||
import type {
|
||||
Entity,
|
||||
EngineDataviewUpdateResult,
|
||||
InitEntityEngineRequestBody,
|
||||
InitEntityEngineResponse,
|
||||
} from '../../../../common/api/entity_analytics/entity_store/engine/init.gen';
|
||||
import type {
|
||||
EntityType,
|
||||
InspectQuery,
|
||||
} from '../../../../common/api/entity_analytics/entity_store/common.gen';
|
||||
} from '../../../../common/api/entity_analytics';
|
||||
import { EngineDescriptorClient } from './saved_object/engine_descriptor';
|
||||
import { ENGINE_STATUS, MAX_SEARCH_RESPONSE_SIZE } from './constants';
|
||||
import { AssetCriticalityEcsMigrationClient } from '../asset_criticality/asset_criticality_migration_client';
|
||||
|
@ -120,7 +118,7 @@ export class EntityStoreDataClient {
|
|||
throw new Error('Task Manager is not available');
|
||||
}
|
||||
|
||||
const { logger, esClient, namespace, taskManager, appClient, dataViewsService } = this.options;
|
||||
const { logger } = this.options;
|
||||
|
||||
await this.riskScoreDataClient.createRiskScoreLatestIndex();
|
||||
|
||||
|
@ -135,8 +133,6 @@ export class EntityStoreDataClient {
|
|||
logger.info(
|
||||
`In namespace ${this.options.namespace}: Initializing entity store for ${entityType}`
|
||||
);
|
||||
const debugLog = (message: string) =>
|
||||
logger.debug(`[Entity Engine] [${entityType}] ${message}`);
|
||||
|
||||
const descriptor = await this.engineClient.init(entityType, {
|
||||
filter,
|
||||
|
@ -144,9 +140,34 @@ export class EntityStoreDataClient {
|
|||
indexPattern,
|
||||
});
|
||||
logger.debug(`Initialized engine for ${entityType}`);
|
||||
const indexPatterns = await buildIndexPatterns(namespace, appClient, dataViewsService);
|
||||
// first create the entity definition without starting it
|
||||
// so that the index template is created which we can add a component template to
|
||||
|
||||
this.asyncSetup(
|
||||
entityType,
|
||||
fieldHistoryLength,
|
||||
this.options.taskManager,
|
||||
indexPattern,
|
||||
filter,
|
||||
pipelineDebugMode
|
||||
).catch((error) => {
|
||||
logger.error('There was an error during async setup of the Entity Store', error);
|
||||
});
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
private async asyncSetup(
|
||||
entityType: EntityType,
|
||||
fieldHistoryLength: number,
|
||||
taskManager: TaskManagerStartContract,
|
||||
indexPattern: string,
|
||||
filter: string,
|
||||
pipelineDebugMode: boolean
|
||||
) {
|
||||
const { esClient, logger, namespace, appClient, dataViewsService } = this.options;
|
||||
const indexPatterns = await buildIndexPatterns(namespace, appClient, dataViewsService);
|
||||
|
||||
const unitedDefinition = getUnitedEntityDefinition({
|
||||
indexPatterns,
|
||||
entityType,
|
||||
|
@ -155,66 +176,84 @@ export class EntityStoreDataClient {
|
|||
});
|
||||
const { entityManagerDefinition } = unitedDefinition;
|
||||
|
||||
await this.entityClient.createEntityDefinition({
|
||||
definition: {
|
||||
...entityManagerDefinition,
|
||||
filter,
|
||||
indexPatterns: indexPattern
|
||||
? [...entityManagerDefinition.indexPatterns, ...indexPattern.split(',')]
|
||||
: entityManagerDefinition.indexPatterns,
|
||||
},
|
||||
installOnly: true,
|
||||
});
|
||||
debugLog(`Created entity definition`);
|
||||
const debugLog = (message: string) =>
|
||||
logger.debug(`[Entity Engine] [${entityType}] ${message}`);
|
||||
|
||||
// the index must be in place with the correct mapping before the enrich policy is created
|
||||
// this is because the enrich policy will fail if the index does not exist with the correct fields
|
||||
await createEntityIndexComponentTemplate({
|
||||
unitedDefinition,
|
||||
esClient,
|
||||
});
|
||||
debugLog(`Created entity index component template`);
|
||||
await createEntityIndex({
|
||||
entityType,
|
||||
esClient,
|
||||
namespace,
|
||||
logger,
|
||||
});
|
||||
debugLog(`Created entity index`);
|
||||
try {
|
||||
// clean up any existing entity store
|
||||
await this.delete(entityType, taskManager, { deleteData: false, deleteEngine: false });
|
||||
|
||||
// we must create and execute the enrich policy before the pipeline is created
|
||||
// this is because the pipeline will fail if the enrich index does not exist
|
||||
await createFieldRetentionEnrichPolicy({
|
||||
unitedDefinition,
|
||||
esClient,
|
||||
});
|
||||
debugLog(`Created field retention enrich policy`);
|
||||
await executeFieldRetentionEnrichPolicy({
|
||||
unitedDefinition,
|
||||
esClient,
|
||||
logger,
|
||||
});
|
||||
debugLog(`Executed field retention enrich policy`);
|
||||
await createPlatformPipeline({
|
||||
debugMode: pipelineDebugMode,
|
||||
unitedDefinition,
|
||||
logger,
|
||||
esClient,
|
||||
});
|
||||
debugLog(`Created @platform pipeline`);
|
||||
// set up the entity manager definition
|
||||
await this.entityClient.createEntityDefinition({
|
||||
definition: {
|
||||
...entityManagerDefinition,
|
||||
filter,
|
||||
indexPatterns: indexPattern
|
||||
? [...entityManagerDefinition.indexPatterns, ...indexPattern.split(',')]
|
||||
: entityManagerDefinition.indexPatterns,
|
||||
},
|
||||
installOnly: true,
|
||||
});
|
||||
debugLog(`Created entity definition`);
|
||||
|
||||
// finally start the entity definition now that everything is in place
|
||||
const updated = await this.start(entityType, { force: true });
|
||||
debugLog(`Started entity definition`);
|
||||
// the index must be in place with the correct mapping before the enrich policy is created
|
||||
// this is because the enrich policy will fail if the index does not exist with the correct fields
|
||||
await createEntityIndexComponentTemplate({
|
||||
unitedDefinition,
|
||||
esClient,
|
||||
});
|
||||
debugLog(`Created entity index component template`);
|
||||
await createEntityIndex({
|
||||
entityType,
|
||||
esClient,
|
||||
namespace,
|
||||
logger,
|
||||
});
|
||||
debugLog(`Created entity index`);
|
||||
|
||||
// the task will execute the enrich policy on a schedule
|
||||
await startEntityStoreFieldRetentionEnrichTask({
|
||||
namespace,
|
||||
logger,
|
||||
taskManager,
|
||||
});
|
||||
logger.info(`Entity store initialized`);
|
||||
return { ...descriptor, ...updated };
|
||||
// we must create and execute the enrich policy before the pipeline is created
|
||||
// this is because the pipeline will fail if the enrich index does not exist
|
||||
await createFieldRetentionEnrichPolicy({
|
||||
unitedDefinition,
|
||||
esClient,
|
||||
});
|
||||
debugLog(`Created field retention enrich policy`);
|
||||
await executeFieldRetentionEnrichPolicy({
|
||||
unitedDefinition,
|
||||
esClient,
|
||||
logger,
|
||||
});
|
||||
debugLog(`Executed field retention enrich policy`);
|
||||
await createPlatformPipeline({
|
||||
debugMode: pipelineDebugMode,
|
||||
unitedDefinition,
|
||||
logger,
|
||||
esClient,
|
||||
});
|
||||
debugLog(`Created @platform pipeline`);
|
||||
|
||||
// finally start the entity definition now that everything is in place
|
||||
const updated = await this.start(entityType, { force: true });
|
||||
debugLog(`Started entity definition`);
|
||||
|
||||
// the task will execute the enrich policy on a schedule
|
||||
await startEntityStoreFieldRetentionEnrichTask({
|
||||
namespace,
|
||||
logger,
|
||||
taskManager,
|
||||
});
|
||||
logger.info(`Entity store initialized`);
|
||||
|
||||
return updated;
|
||||
} catch (err) {
|
||||
this.options.logger.error(
|
||||
`Error initializing entity store for ${entityType}: ${err.message}`
|
||||
);
|
||||
|
||||
await this.engineClient.update(entityType, ENGINE_STATUS.ERROR);
|
||||
|
||||
await this.delete(entityType, taskManager, { deleteData: true, deleteEngine: false });
|
||||
}
|
||||
}
|
||||
|
||||
public async getExistingEntityDefinition(entityType: EntityType) {
|
||||
|
@ -284,9 +323,10 @@ export class EntityStoreDataClient {
|
|||
public async delete(
|
||||
entityType: EntityType,
|
||||
taskManager: TaskManagerStartContract,
|
||||
deleteData: boolean
|
||||
options = { deleteData: false, deleteEngine: true }
|
||||
) {
|
||||
const { namespace, logger, esClient, appClient, dataViewsService } = this.options;
|
||||
const { deleteData, deleteEngine } = options;
|
||||
const descriptor = await this.engineClient.maybeGet(entityType);
|
||||
const indexPatterns = await buildIndexPatterns(namespace, appClient, dataViewsService);
|
||||
const unitedDefinition = getUnitedEntityDefinition({
|
||||
|
@ -328,6 +368,10 @@ export class EntityStoreDataClient {
|
|||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
if (descriptor && deleteEngine) {
|
||||
await this.engineClient.delete(entityType);
|
||||
}
|
||||
// if the last engine then stop the task
|
||||
const { engines } = await this.engineClient.list();
|
||||
if (engines.length === 0) {
|
||||
|
@ -338,10 +382,6 @@ export class EntityStoreDataClient {
|
|||
});
|
||||
}
|
||||
|
||||
if (descriptor) {
|
||||
await this.engineClient.delete(entityType);
|
||||
}
|
||||
|
||||
return { deleted: true };
|
||||
} catch (e) {
|
||||
logger.error(`Error deleting entity store for ${entityType}: ${e.message}`);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -41,8 +41,28 @@ export class EngineDescriptorClient {
|
|||
) {
|
||||
const engineDescriptor = await this.find(entityType);
|
||||
|
||||
if (engineDescriptor.total > 0)
|
||||
throw new Error(`Entity engine for ${entityType} already exists`);
|
||||
if (engineDescriptor.total > 1) {
|
||||
throw new Error(`Found multiple engine descriptors for entity type ${entityType}`);
|
||||
}
|
||||
|
||||
if (engineDescriptor.total === 1) {
|
||||
const old = engineDescriptor.saved_objects[0].attributes;
|
||||
const update = {
|
||||
...old,
|
||||
status: ENGINE_STATUS.INSTALLING,
|
||||
filter,
|
||||
fieldHistoryLength,
|
||||
indexPattern,
|
||||
};
|
||||
await this.deps.soClient.update<EngineDescriptor>(
|
||||
entityEngineDescriptorTypeName,
|
||||
this.getSavedObjectId(entityType),
|
||||
update,
|
||||
{ refresh: 'wait_for' }
|
||||
);
|
||||
|
||||
return update;
|
||||
}
|
||||
|
||||
const { attributes } = await this.deps.soClient.create<EngineDescriptor>(
|
||||
entityEngineDescriptorTypeName,
|
||||
|
|
|
@ -83,7 +83,7 @@ const stackManagementLinks: Array<NodeDefinition<AppDeepLinkId, string, string>>
|
|||
{ link: 'management:watcher' },
|
||||
{ link: 'management:maintenanceWindows' },
|
||||
{ link: `${SECURITY_UI_APP_ID}:${SecurityPageName.entityAnalyticsManagement}` },
|
||||
{ link: `${SECURITY_UI_APP_ID}:${SecurityPageName.entityAnalyticsAssetClassification}` },
|
||||
{ link: `${SECURITY_UI_APP_ID}:${SecurityPageName.entityAnalyticsEntityStoreManagement}` },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -16,7 +16,7 @@ const SecurityManagementCards = new Map<string, CardNavExtensionDefinition['cate
|
|||
[ExternalPageName.visualize, 'content'],
|
||||
[ExternalPageName.maps, 'content'],
|
||||
[SecurityPageName.entityAnalyticsManagement, 'alerts'],
|
||||
[SecurityPageName.entityAnalyticsAssetClassification, 'alerts'],
|
||||
[SecurityPageName.entityAnalyticsEntityStoreManagement, 'alerts'],
|
||||
]);
|
||||
|
||||
export const enableManagementCardsLanding = (services: Services) => {
|
||||
|
|
|
@ -35103,7 +35103,6 @@
|
|||
"xpack.securitySolution.api.riskEngine.taskManagerUnavailable": "Le gestionnaire des tâches n'est pas disponible mais est requis par le moteur de risque. Veuillez autoriser le plug-in du gestionnaire des tâches et essayer à nouveau.",
|
||||
"xpack.securitySolution.appLinks.actionHistoryDescription": "Affichez l'historique des actions de réponse effectuées sur les hôtes.",
|
||||
"xpack.securitySolution.appLinks.alerts": "Alertes",
|
||||
"xpack.securitySolution.appLinks.assetClassificationDescription": "Représente la criticité d'un actif pour l'infrastructure de votre entreprise.",
|
||||
"xpack.securitySolution.appLinks.attackDiscovery": "Attack discovery",
|
||||
"xpack.securitySolution.appLinks.blocklistDescription": "Excluez les applications non souhaitées de l'exécution sur vos hôtes.",
|
||||
"xpack.securitySolution.appLinks.category.cloudSecurity": "Sécurité du cloud",
|
||||
|
@ -38361,7 +38360,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.assetCriticalityResultStep.uploadAnotherFile": "Charger un autre fichier",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.acceptedFileFormats": "Formats de fichiers : {formats}",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledMessage": "Veuillez autoriser \"{ENABLE_ASSET_CRITICALITY_SETTING}\" dans les paramètres avancés pour accéder à la page.",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledTitle": "Cette page est désactivée",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetCriticalityLabels": "Niveau de criticité : Spécifiez n'importe laquelle de ces {labels}",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetIdentifierDescription": "Identificateur : Spécifiez le {hostName} ou le {userName} de l'entité.",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetTypeDescription": "Type d'entité : Veuillez indiquer si l'entité est un {host} ou un {user}.",
|
||||
|
@ -38381,7 +38379,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.resultsStepTitle": "Résultats",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.selectFileStepTitle": "Sélectionner un fichier",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.subTitle": "Importez vos données de criticité des ressources",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.title": "Criticité des ressources",
|
||||
"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}",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityValidationStep.assignButtonText": "Affecter",
|
||||
|
|
|
@ -34848,7 +34848,6 @@
|
|||
"xpack.securitySolution.api.riskEngine.taskManagerUnavailable": "タスクマネージャーは使用できませんが、リスクエンジンには必要です。taskManagerプラグインを有効にして、再試行してください。",
|
||||
"xpack.securitySolution.appLinks.actionHistoryDescription": "ホストで実行された対応アクションの履歴を表示します。",
|
||||
"xpack.securitySolution.appLinks.alerts": "アラート",
|
||||
"xpack.securitySolution.appLinks.assetClassificationDescription": "ビジネスインフラに対するアセットの重要度を表します。",
|
||||
"xpack.securitySolution.appLinks.attackDiscovery": "Attack discovery",
|
||||
"xpack.securitySolution.appLinks.blocklistDescription": "不要なアプリケーションがホストで実行されないようにします。",
|
||||
"xpack.securitySolution.appLinks.category.cloudSecurity": "クラウドセキュリティ",
|
||||
|
@ -38103,7 +38102,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.assetCriticalityResultStep.uploadAnotherFile": "別のファイルをアップロード",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.acceptedFileFormats": "ファイル形式:{formats}",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledMessage": "ページにアクセスするには、詳細設定で\"{ENABLE_ASSET_CRITICALITY_SETTING}\"を有効化してください。",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledTitle": "このページは無効です",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetCriticalityLabels": "重要度レベル:{labels}のいずれかを指定",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetIdentifierDescription": "識別子:エンティティの{hostName}または{userName}を指定します。",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetTypeDescription": "エンティティタイプ:エンティティが{host}か{user}かを示します。",
|
||||
|
@ -38123,7 +38121,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.resultsStepTitle": "結果",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.selectFileStepTitle": "ファイルを選択",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.subTitle": "アセット重要度データをインポート",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.title": "アセット重要度",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unsupportedFileTypeError": "無効なファイル形式が選択されました。{supportedFileExtensions}ファイルを選択して再試行してください。",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.uploadFileSizeLimit": "最大ファイルサイズ:{maxFileSize}",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityValidationStep.assignButtonText": "割り当て",
|
||||
|
|
|
@ -34891,7 +34891,6 @@
|
|||
"xpack.securitySolution.api.riskEngine.taskManagerUnavailable": "任务管理器不可用,但风险引擎需要该管理器。请启用任务管理器插件然后重试。",
|
||||
"xpack.securitySolution.appLinks.actionHistoryDescription": "查看在主机上执行的响应操作的历史记录。",
|
||||
"xpack.securitySolution.appLinks.alerts": "告警",
|
||||
"xpack.securitySolution.appLinks.assetClassificationDescription": "表示资产对您的业务基础设施的关键度。",
|
||||
"xpack.securitySolution.appLinks.attackDiscovery": "Attack Discovery",
|
||||
"xpack.securitySolution.appLinks.blocklistDescription": "阻止不需要的应用程序在您的主机上运行。",
|
||||
"xpack.securitySolution.appLinks.category.cloudSecurity": "云安全",
|
||||
|
@ -38149,7 +38148,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.assetCriticalityResultStep.uploadAnotherFile": "上传另一个文件",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.acceptedFileFormats": "文件格式:{formats}",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledMessage": "请在高级设置上启用“{ENABLE_ASSET_CRITICALITY_SETTING}”以访问此页面。",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledTitle": "已禁用此页面",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetCriticalityLabels": "关键度级别:指定任意 {labels}",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetIdentifierDescription": "标识符:指定实体的 {hostName} 或 {userName}。",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetTypeDescription": "实体类型:指示实体是 {host} 还是 {user}。",
|
||||
|
@ -38169,7 +38167,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.resultsStepTitle": "结果",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.selectFileStepTitle": "选择文件",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.subTitle": "导入资产关键度数据",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.title": "资产关键度",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unsupportedFileTypeError": "选定的文件格式无效。请选择 {supportedFileExtensions} 文件,然后重试",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.uploadFileSizeLimit": "最大文件大小:{maxFileSize}",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityValidationStep.assignButtonText": "分配",
|
||||
|
|
|
@ -31,7 +31,7 @@ describe(
|
|||
});
|
||||
|
||||
it('renders page as expected', () => {
|
||||
cy.get(PAGE_TITLE).should('have.text', 'Asset criticality');
|
||||
cy.get(PAGE_TITLE).should('include.text', 'Entity Store');
|
||||
});
|
||||
|
||||
it('uploads a file', () => {
|
||||
|
|
|
@ -18,25 +18,38 @@ import { RiskScoreEntity } from '../../../tasks/risk_scores/common';
|
|||
import { ENTITY_ANALYTICS_URL } from '../../../urls/navigation';
|
||||
import { PAGE_TITLE } from '../../../screens/entity_analytics_management';
|
||||
|
||||
describe('Enable risk scores from dashboard', { tags: ['@ess', '@serverless'] }, () => {
|
||||
beforeEach(() => {
|
||||
login();
|
||||
visit(ENTITY_ANALYTICS_URL);
|
||||
});
|
||||
describe(
|
||||
'Enable risk scores from dashboard',
|
||||
{
|
||||
tags: ['@ess', '@serverless'],
|
||||
env: {
|
||||
ftrConfig: {
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['entityStoreDisabled'])}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
login();
|
||||
visit(ENTITY_ANALYTICS_URL);
|
||||
});
|
||||
|
||||
it('host risk enable button should redirect to entity management page', () => {
|
||||
cy.get(ENABLE_HOST_RISK_SCORE_BUTTON).should('exist');
|
||||
it('host risk enable button should redirect to entity management page', () => {
|
||||
cy.get(ENABLE_HOST_RISK_SCORE_BUTTON).should('exist');
|
||||
|
||||
clickEnableRiskScore(RiskScoreEntity.host);
|
||||
clickEnableRiskScore(RiskScoreEntity.host);
|
||||
|
||||
cy.get(PAGE_TITLE).should('have.text', 'Entity Risk Score');
|
||||
});
|
||||
cy.get(PAGE_TITLE).should('have.text', 'Entity Risk Score');
|
||||
});
|
||||
|
||||
it('user risk enable button should redirect to entity management page', () => {
|
||||
cy.get(ENABLE_USER_RISK_SCORE_BUTTON).should('exist');
|
||||
it('user risk enable button should redirect to entity management page', () => {
|
||||
cy.get(ENABLE_USER_RISK_SCORE_BUTTON).should('exist');
|
||||
|
||||
clickEnableRiskScore(RiskScoreEntity.user);
|
||||
clickEnableRiskScore(RiskScoreEntity.user);
|
||||
|
||||
cy.get(PAGE_TITLE).should('have.text', 'Entity Risk Score');
|
||||
});
|
||||
});
|
||||
cy.get(PAGE_TITLE).should('have.text', 'Entity Risk Score');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -34,68 +34,81 @@ import { deleteAlertsAndRules } from '../../../tasks/api_calls/common';
|
|||
|
||||
const spaceId = 'default';
|
||||
|
||||
describe('Upgrade risk scores', { tags: ['@ess'] }, () => {
|
||||
beforeEach(() => {
|
||||
login();
|
||||
deleteRiskEngineConfiguration();
|
||||
deleteAlertsAndRules();
|
||||
});
|
||||
|
||||
describe('show upgrade risk button', () => {
|
||||
describe(
|
||||
'Upgrade risk scores',
|
||||
{
|
||||
tags: ['@ess'],
|
||||
env: {
|
||||
ftrConfig: {
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['entityStoreDisabled'])}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.host, spaceId });
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.user, spaceId });
|
||||
installLegacyRiskScoreModule(RiskScoreEntity.host, spaceId);
|
||||
installLegacyRiskScoreModule(RiskScoreEntity.user, spaceId);
|
||||
visitWithTimeRange(ENTITY_ANALYTICS_URL);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.host, spaceId });
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.user, spaceId });
|
||||
cy.task('esArchiverUnload', { archiveName: 'risk_hosts' });
|
||||
cy.task('esArchiverUnload', { archiveName: 'risk_users' });
|
||||
});
|
||||
|
||||
it('shows upgrade panel', () => {
|
||||
cy.get(UPGRADE_RISK_SCORE_BUTTON).should('be.visible');
|
||||
|
||||
clickUpgradeRiskScore();
|
||||
|
||||
cy.get(PAGE_TITLE).should('have.text', 'Entity Risk Score');
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgrade risk engine', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('esArchiverLoad', { archiveName: 'risk_hosts' });
|
||||
cy.task('esArchiverLoad', { archiveName: 'risk_users' });
|
||||
login();
|
||||
installRiskScoreModule();
|
||||
visitWithTimeRange(ENTITY_ANALYTICS_URL);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.task('esArchiverUnload', { archiveName: 'risk_hosts' });
|
||||
cy.task('esArchiverUnload', { archiveName: 'risk_users' });
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.host, spaceId });
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.user, spaceId });
|
||||
deleteRiskEngineConfiguration();
|
||||
deleteAlertsAndRules();
|
||||
});
|
||||
|
||||
it('show old risk score data before upgrade, and hide after', () => {
|
||||
cy.get(HOSTS_TABLE).should('be.visible');
|
||||
cy.get(HOSTS_TABLE_ROWS).should('have.length', 5);
|
||||
describe('show upgrade risk button', () => {
|
||||
beforeEach(() => {
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.host, spaceId });
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.user, spaceId });
|
||||
installLegacyRiskScoreModule(RiskScoreEntity.host, spaceId);
|
||||
installLegacyRiskScoreModule(RiskScoreEntity.user, spaceId);
|
||||
visitWithTimeRange(ENTITY_ANALYTICS_URL);
|
||||
});
|
||||
|
||||
cy.get(USERS_TABLE).should('be.visible');
|
||||
cy.get(USERS_TABLE_ROWS).should('have.length', 5);
|
||||
afterEach(() => {
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.host, spaceId });
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.user, spaceId });
|
||||
cy.task('esArchiverUnload', { archiveName: 'risk_hosts' });
|
||||
cy.task('esArchiverUnload', { archiveName: 'risk_users' });
|
||||
});
|
||||
|
||||
upgradeRiskEngine();
|
||||
it('shows upgrade panel', () => {
|
||||
cy.get(UPGRADE_RISK_SCORE_BUTTON).should('be.visible');
|
||||
|
||||
visitWithTimeRange(ENTITY_ANALYTICS_URL);
|
||||
clickUpgradeRiskScore();
|
||||
|
||||
cy.get(HOST_RISK_SCORE_NO_DATA_DETECTED).should('be.visible');
|
||||
cy.get(USER_RISK_SCORE_NO_DATA_DETECTED).should('be.visible');
|
||||
cy.get(PAGE_TITLE).should('have.text', 'Entity Risk Score');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgrade risk engine', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('esArchiverLoad', { archiveName: 'risk_hosts' });
|
||||
cy.task('esArchiverLoad', { archiveName: 'risk_users' });
|
||||
login();
|
||||
installRiskScoreModule();
|
||||
visitWithTimeRange(ENTITY_ANALYTICS_URL);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.task('esArchiverUnload', { archiveName: 'risk_hosts' });
|
||||
cy.task('esArchiverUnload', { archiveName: 'risk_users' });
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.host, spaceId });
|
||||
deleteRiskScore({ riskScoreEntity: RiskScoreEntity.user, spaceId });
|
||||
deleteRiskEngineConfiguration();
|
||||
});
|
||||
|
||||
it('show old risk score data before upgrade, and hide after', () => {
|
||||
cy.get(HOSTS_TABLE).should('be.visible');
|
||||
cy.get(HOSTS_TABLE_ROWS).should('have.length', 5);
|
||||
|
||||
cy.get(USERS_TABLE).should('be.visible');
|
||||
cy.get(USERS_TABLE_ROWS).should('have.length', 5);
|
||||
|
||||
upgradeRiskEngine();
|
||||
|
||||
visitWithTimeRange(ENTITY_ANALYTICS_URL);
|
||||
|
||||
cy.get(HOST_RISK_SCORE_NO_DATA_DETECTED).should('be.visible');
|
||||
cy.get(USER_RISK_SCORE_NO_DATA_DETECTED).should('be.visible');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { getDataTestSubjectSelector } from '../helpers/common';
|
||||
|
||||
export const PAGE_TITLE = getDataTestSubjectSelector('assetCriticalityUploadPage');
|
||||
export const PAGE_TITLE = getDataTestSubjectSelector('entityStoreManagementPage');
|
||||
export const FILE_PICKER = getDataTestSubjectSelector('asset-criticality-file-picker');
|
||||
export const ASSIGN_BUTTON = getDataTestSubjectSelector('asset-criticality-assign-button');
|
||||
export const RESULT_STEP = getDataTestSubjectSelector('asset-criticality-result-step-success');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue