[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
![Screenshot 2024-10-15 at 12 14
40](https://github.com/user-attachments/assets/90ab2eaa-dd73-47b4-b940-c9549422e37c)

#### Enable Risk score only (Entity store already enabled)
![Screenshot 2024-10-15 at 12 15
04](https://github.com/user-attachments/assets/3ef31857-7515-4636-adde-f6c6e7f7c13b)

#### Modal to choose what to enable
![Screenshot 2024-10-15 at 12 14
48](https://github.com/user-attachments/assets/1746767a-cfb0-41c0-823c-cafac45bd901)


#### New Entity Store management page
![Screenshot 2024-10-15 at 12 14
08](https://github.com/user-attachments/assets/aa2b8c63-1fcf-4a18-87d2-cecceaabd6cd)

---------

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:
Tiago Vila Verde 2024-10-15 17:42:39 +02:00 committed by GitHub
parent f0f1775632
commit 58b2c6ebde
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 1543 additions and 424 deletions

View file

@ -48009,6 +48009,7 @@ components:
- started
- stopped
- updating
- error
type: string
Security_Entity_Analytics_API_Entity:
oneOf:

View file

@ -48009,6 +48009,7 @@ components:
- started
- stopped
- updating
- error
type: string
Security_Entity_Analytics_API_Entity:
oneOf:

View file

@ -56775,6 +56775,7 @@ components:
- started
- stopped
- updating
- error
type: string
Security_Entity_Analytics_API_Entity:
oneOf:

View file

@ -56775,6 +56775,7 @@ components:
- started
- stopped
- updating
- error
type: string
Security_Entity_Analytics_API_Entity:
oneOf:

View file

@ -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',
}

View file

@ -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;

View file

@ -38,6 +38,7 @@ components:
- started
- stopped
- updating
- error
IndexPattern:
type: string

View file

@ -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;

View file

@ -806,6 +806,7 @@ components:
- started
- stopped
- updating
- error
type: string
Entity:
oneOf:

View file

@ -806,6 +806,7 @@ components:
- started
- stopped
- updating
- error
type: string
Entity:
oneOf:

View file

@ -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.
},
];

View file

@ -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[] => {

View file

@ -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',
});

View file

@ -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',
};

View file

@ -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]);
};

View file

@ -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';

View file

@ -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>
);
};

View file

@ -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>

View file

@ -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>

View file

@ -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%',
},
];
};

View file

@ -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,
];

View file

@ -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,

View file

@ -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,
};
};

View file

@ -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);
}
},
}
);
};

View file

@ -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

View file

@ -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';

View file

@ -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>

View file

@ -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>
);
};

View file

@ -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,
},
];

View file

@ -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;
}

View file

@ -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,

View file

@ -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;

View file

@ -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}`);

View file

@ -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) {

View file

@ -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,

View file

@ -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}` },
],
},
{

View file

@ -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) => {

View file

@ -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",

View file

@ -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": "割り当て",

View file

@ -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": "分配",

View file

@ -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', () => {

View 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');
});
}
);

View file

@ -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');
});
});
}
);

View file

@ -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');