mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[SecuritySolution] Entity Engine status tab (#201235)
## Summary * Add two tabs to the Entity Store page * The import entities tab has all the bulk upload content * The status tab has the new content created on this PR * Move the "clear entity store data" button to the header according to design mockups. * Delete unused stats route * Rename `enablement` API docs to `enable` * Add a new parameter to the status API (`withComponents`) * Should I make it snake cased? ### import entities tab  ### status tab  ## How to test it - Open security solution app with data - Go to entity store page - You shouldn't see the new tab because the engine is disabled - Enable the engine and wait - Click on the new tab that showed up - It should list user and host engine components, and everything should be installed - Delete or misconfigure some of the resources, the new status should be reflected on the tab. ## TODO: - [x] Rebase main after https://github.com/elastic/kibana/pull/199762 is merged - [x] Remove temporary status hook - [x] Fix the"clear entity data" button. It should re-fetch the status API. ### Checklist Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
52fa276661
commit
06b7993bd9
39 changed files with 1664 additions and 517 deletions
|
@ -7560,42 +7560,6 @@ paths:
|
|||
tags:
|
||||
- Security Entity Analytics API
|
||||
x-beta: true
|
||||
/api/entity_store/engines/{entityType}/stats:
|
||||
post:
|
||||
operationId: GetEntityEngineStats
|
||||
parameters:
|
||||
- description: The entity type of the engine (either 'user' or 'host').
|
||||
in: path
|
||||
name: entityType
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EntityType'
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
indexPattern:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern'
|
||||
indices:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
status:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineStatus'
|
||||
transforms:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
type:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EntityType'
|
||||
description: Successful response
|
||||
summary: Get Entity Engine stats
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
x-beta: true
|
||||
/api/entity_store/engines/{entityType}/stop:
|
||||
post:
|
||||
operationId: StopEntityEngine
|
||||
|
@ -7749,6 +7713,12 @@ paths:
|
|||
/api/entity_store/status:
|
||||
get:
|
||||
operationId: GetEntityStoreStatus
|
||||
parameters:
|
||||
- description: If true returns a detailed status of the engine including all it's components
|
||||
in: query
|
||||
name: include_components
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
|
@ -7758,10 +7728,20 @@ paths:
|
|||
properties:
|
||||
engines:
|
||||
items:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor'
|
||||
- type: object
|
||||
properties:
|
||||
components:
|
||||
items:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineComponentStatus'
|
||||
type: array
|
||||
type: array
|
||||
status:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_StoreStatus'
|
||||
required:
|
||||
- status
|
||||
- engines
|
||||
description: Successful response
|
||||
summary: Get the status of the Entity Store
|
||||
tags:
|
||||
|
@ -45755,6 +45735,47 @@ components:
|
|||
$ref: '#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel'
|
||||
required:
|
||||
- criticality_level
|
||||
Security_Entity_Analytics_API_EngineComponentResource:
|
||||
enum:
|
||||
- entity_engine
|
||||
- entity_definition
|
||||
- index
|
||||
- component_template
|
||||
- index_template
|
||||
- ingest_pipeline
|
||||
- enrich_policy
|
||||
- task
|
||||
- transform
|
||||
type: string
|
||||
Security_Entity_Analytics_API_EngineComponentStatus:
|
||||
type: object
|
||||
properties:
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
type: array
|
||||
health:
|
||||
enum:
|
||||
- green
|
||||
- yellow
|
||||
- red
|
||||
- unknown
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
installed:
|
||||
type: boolean
|
||||
resource:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineComponentResource'
|
||||
required:
|
||||
- id
|
||||
- installed
|
||||
- resource
|
||||
Security_Entity_Analytics_API_EngineDataviewUpdateResult:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -10445,41 +10445,6 @@ paths:
|
|||
summary: Start an Entity Engine
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_store/engines/{entityType}/stats:
|
||||
post:
|
||||
operationId: GetEntityEngineStats
|
||||
parameters:
|
||||
- description: The entity type of the engine (either 'user' or 'host').
|
||||
in: path
|
||||
name: entityType
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EntityType'
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
indexPattern:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern'
|
||||
indices:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
status:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineStatus'
|
||||
transforms:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
type:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EntityType'
|
||||
description: Successful response
|
||||
summary: Get Entity Engine stats
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_store/engines/{entityType}/stop:
|
||||
post:
|
||||
operationId: StopEntityEngine
|
||||
|
@ -10630,6 +10595,12 @@ paths:
|
|||
/api/entity_store/status:
|
||||
get:
|
||||
operationId: GetEntityStoreStatus
|
||||
parameters:
|
||||
- description: If true returns a detailed status of the engine including all it's components
|
||||
in: query
|
||||
name: include_components
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
|
@ -10639,10 +10610,20 @@ paths:
|
|||
properties:
|
||||
engines:
|
||||
items:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineDescriptor'
|
||||
- type: object
|
||||
properties:
|
||||
components:
|
||||
items:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineComponentStatus'
|
||||
type: array
|
||||
type: array
|
||||
status:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_StoreStatus'
|
||||
required:
|
||||
- status
|
||||
- engines
|
||||
description: Successful response
|
||||
summary: Get the status of the Entity Store
|
||||
tags:
|
||||
|
@ -53478,6 +53459,47 @@ components:
|
|||
$ref: '#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel'
|
||||
required:
|
||||
- criticality_level
|
||||
Security_Entity_Analytics_API_EngineComponentResource:
|
||||
enum:
|
||||
- entity_engine
|
||||
- entity_definition
|
||||
- index
|
||||
- component_template
|
||||
- index_template
|
||||
- ingest_pipeline
|
||||
- enrich_policy
|
||||
- task
|
||||
- transform
|
||||
type: string
|
||||
Security_Entity_Analytics_API_EngineComponentStatus:
|
||||
type: object
|
||||
properties:
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
type: array
|
||||
health:
|
||||
enum:
|
||||
- green
|
||||
- yellow
|
||||
- red
|
||||
- unknown
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
installed:
|
||||
type: boolean
|
||||
resource:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_EngineComponentResource'
|
||||
required:
|
||||
- id
|
||||
- installed
|
||||
- resource
|
||||
Security_Entity_Analytics_API_EngineDataviewUpdateResult:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -39,6 +39,37 @@ export const EngineDescriptor = z.object({
|
|||
error: z.object({}).optional(),
|
||||
});
|
||||
|
||||
export type EngineComponentResource = z.infer<typeof EngineComponentResource>;
|
||||
export const EngineComponentResource = z.enum([
|
||||
'entity_engine',
|
||||
'entity_definition',
|
||||
'index',
|
||||
'component_template',
|
||||
'index_template',
|
||||
'ingest_pipeline',
|
||||
'enrich_policy',
|
||||
'task',
|
||||
'transform',
|
||||
]);
|
||||
export type EngineComponentResourceEnum = typeof EngineComponentResource.enum;
|
||||
export const EngineComponentResourceEnum = EngineComponentResource.enum;
|
||||
|
||||
export type EngineComponentStatus = z.infer<typeof EngineComponentStatus>;
|
||||
export const EngineComponentStatus = z.object({
|
||||
id: z.string(),
|
||||
installed: z.boolean(),
|
||||
resource: EngineComponentResource,
|
||||
health: z.enum(['green', 'yellow', 'red', 'unknown']).optional(),
|
||||
errors: z
|
||||
.array(
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type StoreStatus = z.infer<typeof StoreStatus>;
|
||||
export const StoreStatus = z.enum(['not_installed', 'installing', 'running', 'stopped', 'error']);
|
||||
export type StoreStatusEnum = typeof StoreStatus.enum;
|
||||
|
|
|
@ -42,6 +42,49 @@ components:
|
|||
- updating
|
||||
- error
|
||||
|
||||
EngineComponentStatus:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- installed
|
||||
- resource
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
installed:
|
||||
type: boolean
|
||||
resource:
|
||||
$ref: '#/components/schemas/EngineComponentResource'
|
||||
health:
|
||||
type: string
|
||||
enum:
|
||||
- green
|
||||
- yellow
|
||||
- red
|
||||
- unknown
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
EngineComponentResource:
|
||||
type: string
|
||||
enum:
|
||||
- entity_engine
|
||||
- entity_definition
|
||||
- index
|
||||
- component_template
|
||||
- index_template
|
||||
- ingest_pipeline
|
||||
- enrich_policy
|
||||
- task
|
||||
- transform
|
||||
|
||||
StoreStatus:
|
||||
type: string
|
||||
enum:
|
||||
|
|
|
@ -16,13 +16,7 @@
|
|||
|
||||
import { z } from '@kbn/zod';
|
||||
|
||||
import { IndexPattern, EngineDescriptor, StoreStatus } from './common.gen';
|
||||
|
||||
export type GetEntityStoreStatusResponse = z.infer<typeof GetEntityStoreStatusResponse>;
|
||||
export const GetEntityStoreStatusResponse = z.object({
|
||||
status: StoreStatus.optional(),
|
||||
engines: z.array(EngineDescriptor).optional(),
|
||||
});
|
||||
import { IndexPattern, EngineDescriptor } from './common.gen';
|
||||
|
||||
export type InitEntityStoreRequestBody = z.infer<typeof InitEntityStoreRequestBody>;
|
||||
export const InitEntityStoreRequestBody = z.object({
|
|
@ -41,24 +41,3 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$ref: './common.schema.yaml#/components/schemas/EngineDescriptor'
|
||||
|
||||
/api/entity_store/status:
|
||||
get:
|
||||
x-labels: [ess, serverless]
|
||||
x-codegen-enabled: true
|
||||
operationId: GetEntityStoreStatus
|
||||
summary: Get the status of the Entity Store
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: './common.schema.yaml#/components/schemas/StoreStatus'
|
||||
engines:
|
||||
type: array
|
||||
items:
|
||||
$ref: './common.schema.yaml#/components/schemas/EngineDescriptor'
|
|
@ -10,6 +10,5 @@ export * from './get.gen';
|
|||
export * from './init.gen';
|
||||
export * from './list.gen';
|
||||
export * from './start.gen';
|
||||
export * from './stats.gen';
|
||||
export * from './stop.gen';
|
||||
export * from './apply_dataview_indices.gen';
|
||||
|
|
|
@ -1,39 +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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Get Entity Engine stats
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
|
||||
import { EntityType, IndexPattern, EngineStatus } from '../common.gen';
|
||||
|
||||
export type GetEntityEngineStatsRequestParams = z.infer<typeof GetEntityEngineStatsRequestParams>;
|
||||
export const GetEntityEngineStatsRequestParams = z.object({
|
||||
/**
|
||||
* The entity type of the engine (either 'user' or 'host').
|
||||
*/
|
||||
entityType: EntityType,
|
||||
});
|
||||
export type GetEntityEngineStatsRequestParamsInput = z.input<
|
||||
typeof GetEntityEngineStatsRequestParams
|
||||
>;
|
||||
|
||||
export type GetEntityEngineStatsResponse = z.infer<typeof GetEntityEngineStatsResponse>;
|
||||
export const GetEntityEngineStatsResponse = z.object({
|
||||
type: EntityType.optional(),
|
||||
indexPattern: IndexPattern.optional(),
|
||||
status: EngineStatus.optional(),
|
||||
transforms: z.array(z.object({})).optional(),
|
||||
indices: z.array(z.object({})).optional(),
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
openapi: 3.0.0
|
||||
|
||||
info:
|
||||
title: Get Entity Engine stats
|
||||
version: '2023-10-31'
|
||||
paths:
|
||||
/api/entity_store/engines/{entityType}/stats:
|
||||
post:
|
||||
x-labels: [ess, serverless]
|
||||
x-codegen-enabled: true
|
||||
operationId: GetEntityEngineStats
|
||||
summary: Get Entity Engine stats
|
||||
parameters:
|
||||
- name: entityType
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
$ref: '../common.schema.yaml#/components/schemas/EntityType'
|
||||
description: The entity type of the engine (either 'user' or 'host').
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
$ref : '../common.schema.yaml#/components/schemas/EntityType'
|
||||
indexPattern:
|
||||
$ref : '../common.schema.yaml#/components/schemas/IndexPattern'
|
||||
status:
|
||||
$ref : '../common.schema.yaml#/components/schemas/EngineStatus'
|
||||
transforms:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
indices:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Enable Entity Store
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
import { BooleanFromString } from '@kbn/zod-helpers';
|
||||
|
||||
import { StoreStatus, EngineDescriptor, EngineComponentStatus } from './common.gen';
|
||||
|
||||
export type GetEntityStoreStatusRequestQuery = z.infer<typeof GetEntityStoreStatusRequestQuery>;
|
||||
export const GetEntityStoreStatusRequestQuery = z.object({
|
||||
/**
|
||||
* If true returns a detailed status of the engine including all it's components
|
||||
*/
|
||||
include_components: BooleanFromString.optional(),
|
||||
});
|
||||
export type GetEntityStoreStatusRequestQueryInput = z.input<
|
||||
typeof GetEntityStoreStatusRequestQuery
|
||||
>;
|
||||
|
||||
export type GetEntityStoreStatusResponse = z.infer<typeof GetEntityStoreStatusResponse>;
|
||||
export const GetEntityStoreStatusResponse = z.object({
|
||||
status: StoreStatus,
|
||||
engines: z.array(
|
||||
EngineDescriptor.merge(
|
||||
z.object({
|
||||
components: z.array(EngineComponentStatus).optional(),
|
||||
})
|
||||
)
|
||||
),
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
openapi: 3.0.0
|
||||
|
||||
info:
|
||||
title: Enable Entity Store
|
||||
version: '2023-10-31'
|
||||
paths:
|
||||
/api/entity_store/status:
|
||||
get:
|
||||
x-labels: [ess, serverless]
|
||||
x-codegen-enabled: true
|
||||
operationId: GetEntityStoreStatus
|
||||
summary: Get the status of the Entity Store
|
||||
|
||||
parameters:
|
||||
- name: include_components
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
description: If true returns a detailed status of the engine including all it's components
|
||||
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- engines
|
||||
properties:
|
||||
status:
|
||||
$ref: './common.schema.yaml#/components/schemas/StoreStatus'
|
||||
engines:
|
||||
type: array
|
||||
items:
|
||||
allOf:
|
||||
- $ref: './common.schema.yaml#/components/schemas/EngineDescriptor'
|
||||
- type: object
|
||||
properties:
|
||||
components:
|
||||
type: array
|
||||
items:
|
||||
$ref: './common.schema.yaml#/components/schemas/EngineComponentStatus'
|
|
@ -232,10 +232,9 @@ import type {
|
|||
UploadAssetCriticalityRecordsResponse,
|
||||
} from './entity_analytics/asset_criticality/upload_asset_criticality_csv.gen';
|
||||
import type {
|
||||
GetEntityStoreStatusResponse,
|
||||
InitEntityStoreRequestBodyInput,
|
||||
InitEntityStoreResponse,
|
||||
} from './entity_analytics/entity_store/enablement.gen';
|
||||
} from './entity_analytics/entity_store/enable.gen';
|
||||
import type { ApplyEntityEngineDataviewIndicesResponse } from './entity_analytics/entity_store/engine/apply_dataview_indices.gen';
|
||||
import type {
|
||||
DeleteEntityEngineRequestQueryInput,
|
||||
|
@ -257,10 +256,6 @@ import type {
|
|||
StartEntityEngineRequestParamsInput,
|
||||
StartEntityEngineResponse,
|
||||
} from './entity_analytics/entity_store/engine/start.gen';
|
||||
import type {
|
||||
GetEntityEngineStatsRequestParamsInput,
|
||||
GetEntityEngineStatsResponse,
|
||||
} from './entity_analytics/entity_store/engine/stats.gen';
|
||||
import type {
|
||||
StopEntityEngineRequestParamsInput,
|
||||
StopEntityEngineResponse,
|
||||
|
@ -269,6 +264,10 @@ import type {
|
|||
ListEntitiesRequestQueryInput,
|
||||
ListEntitiesResponse,
|
||||
} from './entity_analytics/entity_store/entities/list_entities.gen';
|
||||
import type {
|
||||
GetEntityStoreStatusRequestQueryInput,
|
||||
GetEntityStoreStatusResponse,
|
||||
} from './entity_analytics/entity_store/status.gen';
|
||||
import type { CleanUpRiskEngineResponse } from './entity_analytics/risk_engine/engine_cleanup_route.gen';
|
||||
import type { DisableRiskEngineResponse } from './entity_analytics/risk_engine/engine_disable_route.gen';
|
||||
import type { EnableRiskEngineResponse } from './entity_analytics/risk_engine/engine_enable_route.gen';
|
||||
|
@ -1290,19 +1289,7 @@ finalize it.
|
|||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
async getEntityEngineStats(props: GetEntityEngineStatsProps) {
|
||||
this.log.info(`${new Date().toISOString()} Calling API GetEntityEngineStats`);
|
||||
return this.kbnClient
|
||||
.request<GetEntityEngineStatsResponse>({
|
||||
path: replaceParams('/api/entity_store/engines/{entityType}/stats', props.params),
|
||||
headers: {
|
||||
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
async getEntityStoreStatus() {
|
||||
async getEntityStoreStatus(props: GetEntityStoreStatusProps) {
|
||||
this.log.info(`${new Date().toISOString()} Calling API GetEntityStoreStatus`);
|
||||
return this.kbnClient
|
||||
.request<GetEntityStoreStatusResponse>({
|
||||
|
@ -1311,6 +1298,8 @@ finalize it.
|
|||
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
|
||||
},
|
||||
method: 'GET',
|
||||
|
||||
query: props.query,
|
||||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
|
@ -2285,8 +2274,8 @@ export interface GetEndpointSuggestionsProps {
|
|||
export interface GetEntityEngineProps {
|
||||
params: GetEntityEngineRequestParamsInput;
|
||||
}
|
||||
export interface GetEntityEngineStatsProps {
|
||||
params: GetEntityEngineStatsRequestParamsInput;
|
||||
export interface GetEntityStoreStatusProps {
|
||||
query: GetEntityStoreStatusRequestQueryInput;
|
||||
}
|
||||
export interface GetNotesProps {
|
||||
query: GetNotesRequestQueryInput;
|
||||
|
|
|
@ -430,41 +430,6 @@ paths:
|
|||
summary: Start an Entity Engine
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_store/engines/{entityType}/stats:
|
||||
post:
|
||||
operationId: GetEntityEngineStats
|
||||
parameters:
|
||||
- description: The entity type of the engine (either 'user' or 'host').
|
||||
in: path
|
||||
name: entityType
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/components/schemas/EntityType'
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
indexPattern:
|
||||
$ref: '#/components/schemas/IndexPattern'
|
||||
indices:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
status:
|
||||
$ref: '#/components/schemas/EngineStatus'
|
||||
transforms:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
type:
|
||||
$ref: '#/components/schemas/EntityType'
|
||||
description: Successful response
|
||||
summary: Get Entity Engine stats
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_store/engines/{entityType}/stop:
|
||||
post:
|
||||
operationId: StopEntityEngine
|
||||
|
@ -615,6 +580,14 @@ paths:
|
|||
/api/entity_store/status:
|
||||
get:
|
||||
operationId: GetEntityStoreStatus
|
||||
parameters:
|
||||
- description: >-
|
||||
If true returns a detailed status of the engine including all it's
|
||||
components
|
||||
in: query
|
||||
name: include_components
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
|
@ -624,10 +597,20 @@ paths:
|
|||
properties:
|
||||
engines:
|
||||
items:
|
||||
$ref: '#/components/schemas/EngineDescriptor'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/EngineDescriptor'
|
||||
- type: object
|
||||
properties:
|
||||
components:
|
||||
items:
|
||||
$ref: '#/components/schemas/EngineComponentStatus'
|
||||
type: array
|
||||
type: array
|
||||
status:
|
||||
$ref: '#/components/schemas/StoreStatus'
|
||||
required:
|
||||
- status
|
||||
- engines
|
||||
description: Successful response
|
||||
summary: Get the status of the Entity Store
|
||||
tags:
|
||||
|
@ -824,6 +807,47 @@ components:
|
|||
$ref: '#/components/schemas/AssetCriticalityLevel'
|
||||
required:
|
||||
- criticality_level
|
||||
EngineComponentResource:
|
||||
enum:
|
||||
- entity_engine
|
||||
- entity_definition
|
||||
- index
|
||||
- component_template
|
||||
- index_template
|
||||
- ingest_pipeline
|
||||
- enrich_policy
|
||||
- task
|
||||
- transform
|
||||
type: string
|
||||
EngineComponentStatus:
|
||||
type: object
|
||||
properties:
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
type: array
|
||||
health:
|
||||
enum:
|
||||
- green
|
||||
- yellow
|
||||
- red
|
||||
- unknown
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
installed:
|
||||
type: boolean
|
||||
resource:
|
||||
$ref: '#/components/schemas/EngineComponentResource'
|
||||
required:
|
||||
- id
|
||||
- installed
|
||||
- resource
|
||||
EngineDataviewUpdateResult:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -430,41 +430,6 @@ paths:
|
|||
summary: Start an Entity Engine
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_store/engines/{entityType}/stats:
|
||||
post:
|
||||
operationId: GetEntityEngineStats
|
||||
parameters:
|
||||
- description: The entity type of the engine (either 'user' or 'host').
|
||||
in: path
|
||||
name: entityType
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/components/schemas/EntityType'
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
indexPattern:
|
||||
$ref: '#/components/schemas/IndexPattern'
|
||||
indices:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
status:
|
||||
$ref: '#/components/schemas/EngineStatus'
|
||||
transforms:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
type:
|
||||
$ref: '#/components/schemas/EntityType'
|
||||
description: Successful response
|
||||
summary: Get Entity Engine stats
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/entity_store/engines/{entityType}/stop:
|
||||
post:
|
||||
operationId: StopEntityEngine
|
||||
|
@ -615,6 +580,14 @@ paths:
|
|||
/api/entity_store/status:
|
||||
get:
|
||||
operationId: GetEntityStoreStatus
|
||||
parameters:
|
||||
- description: >-
|
||||
If true returns a detailed status of the engine including all it's
|
||||
components
|
||||
in: query
|
||||
name: include_components
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
|
@ -624,10 +597,20 @@ paths:
|
|||
properties:
|
||||
engines:
|
||||
items:
|
||||
$ref: '#/components/schemas/EngineDescriptor'
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/EngineDescriptor'
|
||||
- type: object
|
||||
properties:
|
||||
components:
|
||||
items:
|
||||
$ref: '#/components/schemas/EngineComponentStatus'
|
||||
type: array
|
||||
type: array
|
||||
status:
|
||||
$ref: '#/components/schemas/StoreStatus'
|
||||
required:
|
||||
- status
|
||||
- engines
|
||||
description: Successful response
|
||||
summary: Get the status of the Entity Store
|
||||
tags:
|
||||
|
@ -824,6 +807,47 @@ components:
|
|||
$ref: '#/components/schemas/AssetCriticalityLevel'
|
||||
required:
|
||||
- criticality_level
|
||||
EngineComponentResource:
|
||||
enum:
|
||||
- entity_engine
|
||||
- entity_definition
|
||||
- index
|
||||
- component_template
|
||||
- index_template
|
||||
- ingest_pipeline
|
||||
- enrich_policy
|
||||
- task
|
||||
- transform
|
||||
type: string
|
||||
EngineComponentStatus:
|
||||
type: object
|
||||
properties:
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
type: array
|
||||
health:
|
||||
enum:
|
||||
- green
|
||||
- yellow
|
||||
- red
|
||||
- unknown
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
installed:
|
||||
type: boolean
|
||||
resource:
|
||||
$ref: '#/components/schemas/EngineComponentResource'
|
||||
required:
|
||||
- id
|
||||
- installed
|
||||
- resource
|
||||
EngineDataviewUpdateResult:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -5,15 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import type { GetEntityStoreStatusResponse } from '../../../common/api/entity_analytics/entity_store/status.gen';
|
||||
import type {
|
||||
GetEntityStoreStatusResponse,
|
||||
InitEntityStoreRequestBody,
|
||||
InitEntityStoreResponse,
|
||||
} from '../../../common/api/entity_analytics/entity_store/enablement.gen';
|
||||
} from '../../../common/api/entity_analytics/entity_store/enable.gen';
|
||||
import type {
|
||||
DeleteEntityEngineResponse,
|
||||
EntityType,
|
||||
GetEntityEngineResponse,
|
||||
InitEntityEngineResponse,
|
||||
ListEntityEnginesResponse,
|
||||
StopEntityEngineResponse,
|
||||
|
@ -35,10 +34,11 @@ export const useEntityStoreRoutes = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const getEntityStoreStatus = async () => {
|
||||
const getEntityStoreStatus = async (withComponents = false) => {
|
||||
return http.fetch<GetEntityStoreStatusResponse>('/api/entity_store/status', {
|
||||
method: 'GET',
|
||||
version: API_VERSIONS.public.v1,
|
||||
query: { include_components: withComponents },
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -58,13 +58,6 @@ export const useEntityStoreRoutes = () => {
|
|||
});
|
||||
};
|
||||
|
||||
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, deleteData: boolean) => {
|
||||
return http.fetch<DeleteEntityEngineResponse>(`/api/entity_store/engines/${entityType}`, {
|
||||
method: 'DELETE',
|
||||
|
@ -85,7 +78,6 @@ export const useEntityStoreRoutes = () => {
|
|||
getEntityStoreStatus,
|
||||
initEntityEngine,
|
||||
stopEntityEngine,
|
||||
getEntityEngine,
|
||||
deleteEntityEngine,
|
||||
listEntityEngines,
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/enablement.gen';
|
||||
import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/status.gen';
|
||||
import type { StoreStatus } from '../../../../../common/api/entity_analytics';
|
||||
import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics';
|
||||
import { useInitRiskEngineMutation } from '../../../api/hooks/use_init_risk_engine_mutation';
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { EngineComponentsStatusTable } from './engine_components_status';
|
||||
import {
|
||||
EngineComponentResourceEnum,
|
||||
type EngineComponentStatus,
|
||||
} from '../../../../../../../common/api/entity_analytics';
|
||||
import { TestProviders } from '../../../../../../common/mock';
|
||||
|
||||
const uninstalledWithErrorsComponent = {
|
||||
id: 'entity_engine_id',
|
||||
installed: false,
|
||||
resource: EngineComponentResourceEnum.entity_engine,
|
||||
errors: [{ title: 'Error 1', message: 'Error message 1' }],
|
||||
};
|
||||
|
||||
const installedComponent = {
|
||||
id: 'index_id',
|
||||
resource: EngineComponentResourceEnum.index,
|
||||
errors: [],
|
||||
installed: true,
|
||||
};
|
||||
|
||||
const mockComponents: EngineComponentStatus[] = [
|
||||
uninstalledWithErrorsComponent,
|
||||
installedComponent,
|
||||
];
|
||||
|
||||
const mockGetUrlForApp = jest.fn();
|
||||
|
||||
jest.mock('../../../../../../common/lib/kibana', () => {
|
||||
return {
|
||||
useKibana: jest.fn().mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
getUrlForApp: () => mockGetUrlForApp(),
|
||||
navigateToApp: jest.fn(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('EngineComponentsStatusTable', () => {
|
||||
it('renders the table with components', () => {
|
||||
render(<EngineComponentsStatusTable components={mockComponents} />, { wrapper: TestProviders });
|
||||
expect(screen.getByTestId('engine-components-status-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('expands and collapses rows correctly', () => {
|
||||
render(<EngineComponentsStatusTable components={mockComponents} />, { wrapper: TestProviders });
|
||||
|
||||
const toggleButton = screen.getByLabelText('Expand');
|
||||
fireEvent.click(toggleButton);
|
||||
|
||||
expect(screen.getByText('Error 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Error message 1')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(toggleButton);
|
||||
|
||||
expect(screen.queryByText('Error 1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Error message 1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('columns', () => {
|
||||
it('renders the correct resource text', () => {
|
||||
render(<EngineComponentsStatusTable components={[installedComponent]} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(screen.getByText('Index')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders checkmark on installation column when installed', () => {
|
||||
render(<EngineComponentsStatusTable components={[installedComponent]} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
const icon = screen.getByTestId('installation-status');
|
||||
|
||||
expect(icon).toHaveAttribute('data-euiicon-type', 'check');
|
||||
});
|
||||
|
||||
it('renders cross on installation column when installed', () => {
|
||||
render(<EngineComponentsStatusTable components={[uninstalledWithErrorsComponent]} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
const icon = screen.getByTestId('installation-status');
|
||||
|
||||
expect(icon).toHaveAttribute('data-euiicon-type', 'cross');
|
||||
});
|
||||
|
||||
it('renders the correct health status', () => {
|
||||
render(<EngineComponentsStatusTable components={[installedComponent]} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('img', { name: /health/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the correct identifier link', () => {
|
||||
mockGetUrlForApp.mockReturnValue('mockedUrl');
|
||||
|
||||
render(<EngineComponentsStatusTable components={[installedComponent]} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', 'mockedUrl');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useState, useMemo, useCallback, Fragment } from 'react';
|
||||
import { EuiSpacer, EuiHealth, EuiCodeBlock } from '@elastic/eui';
|
||||
import { BasicTable } from '../../../../../../common/components/ml/tables/basic_table';
|
||||
import { useColumns } from '../hooks/use_columns';
|
||||
import type { EngineComponentStatus } from '../../../../../../../common/api/entity_analytics';
|
||||
|
||||
type ExpandedRowMap = Record<string, ReactNode>;
|
||||
|
||||
const componentToId = ({ id, resource }: EngineComponentStatus) => `${resource}-${id}`;
|
||||
|
||||
export const EngineComponentsStatusTable = ({
|
||||
components,
|
||||
}: {
|
||||
components: EngineComponentStatus[];
|
||||
}) => {
|
||||
const [expandedItems, setExpandedItems] = useState<EngineComponentStatus[]>([]);
|
||||
|
||||
const itemIdToExpandedRowMap: ExpandedRowMap = useMemo(() => {
|
||||
return expandedItems.reduce<ExpandedRowMap>((acc, componentStatus) => {
|
||||
if (componentStatus.errors && componentStatus.errors.length > 0) {
|
||||
acc[componentToId(componentStatus)] = (
|
||||
<TransformExtendedData errors={componentStatus.errors} />
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}, [expandedItems]);
|
||||
|
||||
const onToggle = useCallback(
|
||||
(component: EngineComponentStatus) => {
|
||||
const isItemExpanded = expandedItems.includes(component);
|
||||
|
||||
if (isItemExpanded) {
|
||||
setExpandedItems(expandedItems.filter((item) => component !== item));
|
||||
} else {
|
||||
setExpandedItems([...expandedItems, component]);
|
||||
}
|
||||
},
|
||||
[expandedItems]
|
||||
);
|
||||
|
||||
const columns = useColumns(onToggle, expandedItems);
|
||||
|
||||
return (
|
||||
<BasicTable
|
||||
data-test-subj="engine-components-status-table"
|
||||
columns={columns}
|
||||
itemId={componentToId}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
items={components}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TransformExtendedData = ({ errors }: { errors: EngineComponentStatus['errors'] }) => {
|
||||
return (
|
||||
<>
|
||||
{errors?.map(({ title, message }) => (
|
||||
<Fragment key={title}>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiHealth color="danger">{title}</EuiHealth>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCodeBlock>{message}</EuiCodeBlock>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IconColor } from '@elastic/eui';
|
||||
import {
|
||||
EuiLink,
|
||||
type EuiBasicTableColumn,
|
||||
EuiHealth,
|
||||
EuiScreenReaderOnly,
|
||||
EuiButtonIcon,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EngineComponentResourceEnum,
|
||||
type EngineComponentResource,
|
||||
} from '../../../../../../../common/api/entity_analytics';
|
||||
import type { EngineComponentStatus } from '../../../../../../../common/api/entity_analytics';
|
||||
import { useKibana } from '../../../../../../common/lib/kibana';
|
||||
|
||||
type TableColumn = EuiBasicTableColumn<EngineComponentStatus>;
|
||||
|
||||
export const HEALTH_COLOR: Record<Required<EngineComponentStatus>['health'], IconColor> = {
|
||||
green: 'success',
|
||||
unknown: 'subdued',
|
||||
yellow: 'warning',
|
||||
red: 'danger',
|
||||
} as const;
|
||||
|
||||
const RESOURCE_TO_TEXT: Record<EngineComponentResource, string> = {
|
||||
ingest_pipeline: 'Ingest Pipeline',
|
||||
enrich_policy: 'Enrich Policy',
|
||||
index: 'Index',
|
||||
component_template: 'Component Template',
|
||||
task: 'Task',
|
||||
transform: 'Transform',
|
||||
entity_definition: 'Entity Definition',
|
||||
entity_engine: 'Engine',
|
||||
index_template: 'Index Template',
|
||||
};
|
||||
|
||||
export const useColumns = (
|
||||
onToggleExpandedItem: (item: EngineComponentStatus) => void,
|
||||
expandedItems: EngineComponentStatus[]
|
||||
): TableColumn[] => {
|
||||
const { getUrlForApp } = useKibana().services.application;
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'resource',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStore.enginesStatus.resourceColumnTitle"
|
||||
defaultMessage="Resource"
|
||||
/>
|
||||
),
|
||||
width: '20%',
|
||||
render: (resource: EngineComponentStatus['resource']) => RESOURCE_TO_TEXT[resource],
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStore.enginesStatus.idColumnTitle"
|
||||
defaultMessage="Identifier"
|
||||
/>
|
||||
),
|
||||
render: (id: EngineComponentStatus['id'], { resource, installed }) => {
|
||||
const path = getResourcePath(id, resource);
|
||||
|
||||
if (!installed || !path) {
|
||||
return id;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href={getUrlForApp('management', {
|
||||
path,
|
||||
})}
|
||||
>
|
||||
{id}
|
||||
</EuiLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'installed',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStore.enginesStatus.installedColumnTitle"
|
||||
defaultMessage="Installed"
|
||||
/>
|
||||
),
|
||||
width: '10%',
|
||||
align: 'center',
|
||||
render: (value: boolean) =>
|
||||
value ? (
|
||||
<EuiIcon data-test-subj="installation-status" type="check" color="success" size="l" />
|
||||
) : (
|
||||
<EuiIcon data-test-subj="installation-status" type="cross" color="danger" size="l" />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStore.enginesStatus.healthColumnTitle"
|
||||
defaultMessage="Health"
|
||||
/>
|
||||
),
|
||||
width: '10%',
|
||||
align: 'center',
|
||||
render: ({ installed, resource, health }: EngineComponentStatus) => {
|
||||
if (!installed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <EuiHealth color={HEALTH_COLOR[health ?? 'green']} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
isExpander: true,
|
||||
align: 'right',
|
||||
width: '40px',
|
||||
name: (
|
||||
<EuiScreenReaderOnly>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStore.enginesStatus.expandRow"
|
||||
defaultMessage="Expand row"
|
||||
/>
|
||||
</span>
|
||||
</EuiScreenReaderOnly>
|
||||
),
|
||||
mobileOptions: { header: false },
|
||||
render: (component: EngineComponentStatus) => {
|
||||
const isItemExpanded = expandedItems.includes(component);
|
||||
|
||||
return component.errors && component.errors.length > 0 ? (
|
||||
<EuiButtonIcon
|
||||
onClick={() => onToggleExpandedItem(component)}
|
||||
aria-label={isItemExpanded ? 'Collapse' : 'Expand'}
|
||||
iconType={isItemExpanded ? 'arrowDown' : 'arrowRight'}
|
||||
/>
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
],
|
||||
[expandedItems, getUrlForApp, onToggleExpandedItem]
|
||||
);
|
||||
};
|
||||
|
||||
const getResourcePath = (id: string, resource: EngineComponentResource) => {
|
||||
if (resource === EngineComponentResourceEnum.ingest_pipeline) {
|
||||
return `ingest/ingest_pipelines?pipeline=${id}`;
|
||||
}
|
||||
|
||||
if (resource === EngineComponentResourceEnum.index_template) {
|
||||
return `data/index_management/templates/${id}`;
|
||||
}
|
||||
|
||||
if (resource === EngineComponentResourceEnum.index) {
|
||||
return `data/index_management/indices/index_details?indexName=${id}`;
|
||||
}
|
||||
|
||||
if (resource === EngineComponentResourceEnum.component_template) {
|
||||
return `data/index_management/component_templates/${id}`;
|
||||
}
|
||||
|
||||
if (resource === EngineComponentResourceEnum.enrich_policy) {
|
||||
return `data/index_management/enrich_policies?policy=${id}`;
|
||||
}
|
||||
|
||||
if (resource === EngineComponentResourceEnum.transform) {
|
||||
return `data/transform/enrich_policies?_a=(transform:(queryText:'${id}'))`;
|
||||
}
|
||||
return null;
|
||||
};
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { EngineStatus } from '.';
|
||||
|
||||
import { TestProviders } from '@kbn/timelines-plugin/public/mock';
|
||||
|
||||
const mockUseEntityStore = jest.fn();
|
||||
jest.mock('../../hooks/use_entity_store', () => ({
|
||||
useEntityStoreStatus: () => mockUseEntityStore(),
|
||||
}));
|
||||
|
||||
const mockDownloadBlob = jest.fn();
|
||||
jest.mock('../../../../../common/utils/download_blob', () => ({
|
||||
downloadBlob: () => mockDownloadBlob(),
|
||||
}));
|
||||
|
||||
describe('EngineStatus', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders loading spinner when data is loading', () => {
|
||||
mockUseEntityStore.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(<EngineStatus />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders error state when there is an error', () => {
|
||||
mockUseEntityStore.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: new Error('Error'),
|
||||
});
|
||||
|
||||
render(<EngineStatus />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.getByText('There was an error loading the engine status')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "No engines found" message when there are no engines', () => {
|
||||
mockUseEntityStore.mockReturnValue({
|
||||
data: { engines: [] },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(<EngineStatus />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.getByText('No engines found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders engine components when data is available', () => {
|
||||
const mockData = {
|
||||
engines: [
|
||||
{
|
||||
type: 'test',
|
||||
components: [{ id: 'entity_engine_id', installed: true, resource: 'entity_engine' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
mockUseEntityStore.mockReturnValue({ data: mockData, isLoading: false, error: null });
|
||||
|
||||
render(<EngineStatus />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.getByText('Test Store')).toBeInTheDocument();
|
||||
expect(screen.getByText('Download status')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls downloadJson when download button is clicked', () => {
|
||||
const mockData = {
|
||||
engines: [
|
||||
{
|
||||
type: 'test',
|
||||
components: [{ id: 'entity_engine_id', installed: true, resource: 'entity_engine' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
mockUseEntityStore.mockReturnValue({ data: mockData, isLoading: false, error: null });
|
||||
|
||||
render(<EngineStatus />, { wrapper: TestProviders });
|
||||
|
||||
const downloadButton = screen.getByText('Download status');
|
||||
fireEvent.click(downloadButton);
|
||||
|
||||
expect(mockDownloadBlob).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import {
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { capitalize } from 'lodash/fp';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useErrorToast } from '../../../../../common/hooks/use_error_toast';
|
||||
import { downloadBlob } from '../../../../../common/utils/download_blob';
|
||||
import { EngineComponentsStatusTable } from './components/engine_components_status';
|
||||
import { useEntityStoreStatus } from '../../hooks/use_entity_store';
|
||||
|
||||
const FILE_NAME = 'engines_status.json';
|
||||
|
||||
export const EngineStatus: React.FC = () => {
|
||||
const { data, isLoading, error } = useEntityStoreStatus({ withComponents: true });
|
||||
|
||||
const downloadJson = () => {
|
||||
downloadBlob(new Blob([JSON.stringify(data)]), FILE_NAME);
|
||||
};
|
||||
|
||||
const errorMessage = i18n.translate(
|
||||
'xpack.securitySolution.entityAnalytics.entityStore.enginesStatus.queryError',
|
||||
{
|
||||
defaultMessage: 'There was an error loading the engine status',
|
||||
}
|
||||
);
|
||||
|
||||
useErrorToast(errorMessage, error);
|
||||
|
||||
if (error) {
|
||||
return <EuiCallOut title={errorMessage} color="danger" iconType="alert" />;
|
||||
}
|
||||
|
||||
if (!data || isLoading) return <EuiLoadingSpinner size="xl" />;
|
||||
|
||||
if (data.engines.length === 0) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStore.enginesStatus.notFoundMessage"
|
||||
defaultMessage="No engines found"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
{data?.engines?.length > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty size="s" onClick={downloadJson}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStore.enginesStatus.downloadButton"
|
||||
defaultMessage="Download status"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
{(data?.engines ?? []).map((engine) => (
|
||||
<Fragment key={engine.type}>
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStore.enginesStatus.title"
|
||||
defaultMessage="{type} Store"
|
||||
values={{
|
||||
type: capitalize(engine.type),
|
||||
}}
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiPanel hasShadow={false} hasBorder={false}>
|
||||
{engine.components && <EngineComponentsStatusTable components={engine.components} />}
|
||||
</EuiPanel>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
</Fragment>
|
||||
))}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -5,13 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { UseMutationOptions, UseQueryOptions } from '@tanstack/react-query';
|
||||
import type { UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import type {
|
||||
GetEntityStoreStatusResponse,
|
||||
InitEntityStoreResponse,
|
||||
} from '../../../../../common/api/entity_analytics/entity_store/enablement.gen';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/status.gen';
|
||||
import type { InitEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/enable.gen';
|
||||
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
|
||||
import type {
|
||||
DeleteEntityEngineResponse,
|
||||
|
@ -26,19 +25,24 @@ const ENTITY_STORE_STATUS = ['GET', 'ENTITY_STORE_STATUS'];
|
|||
interface ResponseError {
|
||||
body: { message: string };
|
||||
}
|
||||
export const useEntityStoreStatus = (options: UseQueryOptions<GetEntityStoreStatusResponse>) => {
|
||||
|
||||
interface Options {
|
||||
withComponents?: boolean;
|
||||
}
|
||||
|
||||
export const useEntityStoreStatus = (opts: Options = {}) => {
|
||||
const { getEntityStoreStatus } = useEntityStoreRoutes();
|
||||
|
||||
const query = useQuery<GetEntityStoreStatusResponse>(ENTITY_STORE_STATUS, getEntityStoreStatus, {
|
||||
return useQuery<GetEntityStoreStatusResponse, IHttpFetchError>({
|
||||
queryKey: [...ENTITY_STORE_STATUS, opts.withComponents],
|
||||
queryFn: () => getEntityStoreStatus(opts.withComponents),
|
||||
refetchInterval: (data) => {
|
||||
if (data?.status === 'installing') {
|
||||
return 5000;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
...options,
|
||||
});
|
||||
return query;
|
||||
};
|
||||
|
||||
export const ENABLE_STORE_STATUS_KEY = ['POST', 'ENABLE_ENTITY_STORE'];
|
||||
|
@ -102,15 +106,18 @@ export const useStopEntityEngineMutation = (options?: UseMutationOptions<{}>) =>
|
|||
};
|
||||
|
||||
export const DELETE_ENTITY_ENGINE_STATUS_KEY = ['POST', 'STOP_ENTITY_ENGINE'];
|
||||
export const useDeleteEntityEngineMutation = (options?: UseMutationOptions<{}>) => {
|
||||
export const useDeleteEntityEngineMutation = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { deleteEntityEngine } = useEntityStoreRoutes();
|
||||
|
||||
return useMutation<DeleteEntityEngineResponse[]>(
|
||||
() => Promise.all([deleteEntityEngine('user', true), deleteEntityEngine('host', true)]),
|
||||
{
|
||||
mutationKey: DELETE_ENTITY_ENGINE_STATUS_KEY,
|
||||
onSuccess: () => queryClient.refetchQueries({ queryKey: ENTITY_STORE_STATUS }),
|
||||
...options,
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ENTITY_STORE_STATUS });
|
||||
onSuccess?.();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { EntityStoreManagementPage } from './entity_store_management_page';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
|
||||
jest.mock('../components/entity_store/components/engines_status', () => ({
|
||||
EngineStatus: () => <span>{'Mocked Engine Status Tab'}</span>,
|
||||
}));
|
||||
|
||||
const mockUseAssetCriticalityPrivileges = jest.fn();
|
||||
jest.mock('../components/asset_criticality/use_asset_criticality', () => ({
|
||||
useAssetCriticalityPrivileges: () => mockUseAssetCriticalityPrivileges(),
|
||||
}));
|
||||
|
||||
const mockUseIsExperimentalFeatureEnabled = jest.fn();
|
||||
jest.mock('../../common/hooks/use_experimental_features', () => ({
|
||||
useIsExperimentalFeatureEnabled: () => mockUseIsExperimentalFeatureEnabled(),
|
||||
}));
|
||||
|
||||
const mockUseHasSecurityCapability = jest.fn().mockReturnValue(true);
|
||||
jest.mock('../../helper_hooks', () => ({
|
||||
useHasSecurityCapability: () => mockUseHasSecurityCapability(),
|
||||
}));
|
||||
|
||||
const mockUseEntityStoreStatus = jest.fn();
|
||||
jest.mock('../components/entity_store/hooks/use_entity_store', () => ({
|
||||
...jest.requireActual('../components/entity_store/hooks/use_entity_store'),
|
||||
useEntityStoreStatus: () => mockUseEntityStoreStatus(),
|
||||
}));
|
||||
|
||||
const mockUseEntityEnginePrivileges = jest.fn();
|
||||
jest.mock('../components/entity_store/hooks/use_entity_engine_privileges', () => ({
|
||||
useEntityEnginePrivileges: () => mockUseEntityEnginePrivileges(),
|
||||
}));
|
||||
|
||||
describe('EntityStoreManagementPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseAssetCriticalityPrivileges.mockReturnValue({
|
||||
isLoading: false,
|
||||
data: { has_write_permissions: true },
|
||||
});
|
||||
|
||||
mockUseEntityEnginePrivileges.mockReturnValue({
|
||||
data: { has_all_required: true },
|
||||
});
|
||||
|
||||
mockUseEntityStoreStatus.mockReturnValue({
|
||||
data: {
|
||||
status: 'running',
|
||||
},
|
||||
errors: [],
|
||||
});
|
||||
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('does not render page when asset criticality is loading', () => {
|
||||
mockUseAssetCriticalityPrivileges.mockReturnValue({
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
render(<EntityStoreManagementPage />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.queryByTestId('entityStoreManagementPage')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the page header', () => {
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
|
||||
|
||||
render(<EntityStoreManagementPage />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.getByTestId('entityStoreManagementPage')).toBeInTheDocument();
|
||||
expect(screen.getByText('Entity Store')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables the switch when status is installing', () => {
|
||||
mockUseEntityStoreStatus.mockReturnValue({
|
||||
data: {
|
||||
status: 'installing',
|
||||
},
|
||||
errors: [],
|
||||
});
|
||||
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
|
||||
|
||||
render(<EntityStoreManagementPage />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.getByTestId('entity-store-switch')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('show clear entity data modal when clear data button clicked', () => {
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
|
||||
|
||||
render(<EntityStoreManagementPage />, { wrapper: TestProviders });
|
||||
|
||||
fireEvent.click(screen.getByText('Clear Entity Data'));
|
||||
|
||||
expect(screen.getByText('Clear Entity data?')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the AssetCriticalityIssueCallout when there is an error', () => {
|
||||
mockUseAssetCriticalityPrivileges.mockReturnValue({
|
||||
isLoading: false,
|
||||
error: { body: { message: 'Error message', status_code: 403 } },
|
||||
});
|
||||
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
|
||||
|
||||
render(<EntityStoreManagementPage />, { wrapper: TestProviders });
|
||||
|
||||
expect(
|
||||
screen.getByText('Asset criticality CSV file upload functionality unavailable.')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Error message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the InsufficientAssetCriticalityPrivilegesCallout when there are no write permissions', () => {
|
||||
mockUseAssetCriticalityPrivileges.mockReturnValue({
|
||||
isLoading: false,
|
||||
data: { has_write_permissions: false },
|
||||
});
|
||||
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
|
||||
|
||||
render(<EntityStoreManagementPage />, { wrapper: TestProviders });
|
||||
|
||||
expect(
|
||||
screen.getByText('Insufficient index privileges to perform CSV upload')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('selects the Import tab by default', () => {
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
|
||||
|
||||
render(<EntityStoreManagementPage />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.getByText('Import Entities').parentNode).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('switches to the Status tab when clicked', () => {
|
||||
mockUseEntityStoreStatus.mockReturnValue({
|
||||
data: {
|
||||
status: 'running',
|
||||
},
|
||||
errors: [],
|
||||
});
|
||||
|
||||
mockUseEntityEnginePrivileges.mockReturnValue({
|
||||
data: { has_all_required: true },
|
||||
});
|
||||
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
|
||||
|
||||
render(<EntityStoreManagementPage />, { wrapper: TestProviders });
|
||||
|
||||
fireEvent.click(screen.getByText('Engine Status'));
|
||||
|
||||
expect(screen.getByText('Engine Status').parentNode).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('does not render the Status tab when entity store is not installed', () => {
|
||||
mockUseEntityStoreStatus.mockReturnValue({
|
||||
data: {
|
||||
status: 'not_installed',
|
||||
},
|
||||
errors: [],
|
||||
});
|
||||
|
||||
mockUseEntityEnginePrivileges.mockReturnValue({
|
||||
data: { has_all_required: true },
|
||||
});
|
||||
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
|
||||
|
||||
render(<EntityStoreManagementPage />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.queryByText('Engine Status')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the Status tab when privileges are missing', () => {
|
||||
mockUseEntityStoreStatus.mockReturnValue({
|
||||
data: {
|
||||
status: 'running',
|
||||
},
|
||||
errors: [],
|
||||
});
|
||||
|
||||
mockUseEntityEnginePrivileges.mockReturnValue({
|
||||
data: { has_all_required: false, privileges: { kibana: {}, elasticsearch: {} } },
|
||||
});
|
||||
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(false);
|
||||
|
||||
render(<EntityStoreManagementPage />, { wrapper: TestProviders });
|
||||
|
||||
expect(screen.queryByText('Engine Status')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -21,13 +21,15 @@ import {
|
|||
EuiCode,
|
||||
EuiSwitch,
|
||||
EuiHealth,
|
||||
EuiButton,
|
||||
EuiLoadingSpinner,
|
||||
EuiToolTip,
|
||||
EuiBetaBadge,
|
||||
EuiTabs,
|
||||
EuiTab,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { SecurityAppError } from '@kbn/securitysolution-t-grid';
|
||||
|
@ -47,11 +49,18 @@ import {
|
|||
import { TECHNICAL_PREVIEW, TECHNICAL_PREVIEW_TOOLTIP } from '../../common/translations';
|
||||
import { useEntityEnginePrivileges } from '../components/entity_store/hooks/use_entity_engine_privileges';
|
||||
import { MissingPrivilegesCallout } from '../components/entity_store/components/missing_privileges_callout';
|
||||
import { EngineStatus } from '../components/entity_store/components/engines_status';
|
||||
|
||||
enum TabId {
|
||||
Import = 'import',
|
||||
Status = 'status',
|
||||
}
|
||||
|
||||
const isSwitchDisabled = (status?: StoreStatus) => status === 'error' || status === 'installing';
|
||||
const isEntityStoreEnabled = (status?: StoreStatus) => status === 'running';
|
||||
const canDeleteEntityEngine = (status?: StoreStatus) =>
|
||||
!['not_installed', 'installing'].includes(status || '');
|
||||
const isEntityStoreInstalled = (status?: StoreStatus) => status && status !== 'not_installed';
|
||||
|
||||
export const EntityStoreManagementPage = () => {
|
||||
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
|
||||
|
@ -62,7 +71,7 @@ export const EntityStoreManagementPage = () => {
|
|||
isLoading: assetCriticalityIsLoading,
|
||||
} = useAssetCriticalityPrivileges('AssetCriticalityUploadPage');
|
||||
const hasAssetCriticalityWritePermissions = assetCriticalityPrivileges?.has_write_permissions;
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState(TabId.Import);
|
||||
const entityStoreStatus = useEntityStoreStatus({});
|
||||
|
||||
const enableStoreMutation = useEnableEntityStoreMutation();
|
||||
|
@ -91,6 +100,15 @@ export const EntityStoreManagementPage = () => {
|
|||
|
||||
const { data: privileges } = useEntityEnginePrivileges();
|
||||
|
||||
const shouldDisplayEngineStatusTab =
|
||||
isEntityStoreInstalled(entityStoreStatus.data?.status) && privileges?.has_all_required;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTabId === TabId.Status && !shouldDisplayEngineStatusTab) {
|
||||
setSelectedTabId(TabId.Import);
|
||||
}
|
||||
}, [shouldDisplayEngineStatusTab, selectedTabId]);
|
||||
|
||||
if (assetCriticalityIsLoading) {
|
||||
// Wait for permission before rendering content to avoid flickering
|
||||
return null;
|
||||
|
@ -149,8 +167,18 @@ export const EntityStoreManagementPage = () => {
|
|||
onSwitch={onSwitchClick}
|
||||
status={entityStoreStatus.data?.status}
|
||||
/>,
|
||||
canDeleteEntityEngine(entityStoreStatus.data?.status) ? (
|
||||
<ClearEntityDataButton
|
||||
{...{
|
||||
deleteEntityEngineMutation,
|
||||
isClearModalVisible,
|
||||
closeClearModal,
|
||||
showClearModal,
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
]
|
||||
: []
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
|
@ -169,14 +197,44 @@ export const EntityStoreManagementPage = () => {
|
|||
</>
|
||||
)}
|
||||
|
||||
<EuiHorizontalRule />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiTabs data-test-subj="tabs">
|
||||
<EuiTab
|
||||
key={TabId.Import}
|
||||
isSelected={selectedTabId === TabId.Import}
|
||||
onClick={() => setSelectedTabId(TabId.Import)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.importEntities.tabTitle"
|
||||
defaultMessage="Import Entities"
|
||||
/>
|
||||
</EuiTab>
|
||||
|
||||
{shouldDisplayEngineStatusTab && (
|
||||
<EuiTab
|
||||
key={TabId.Status}
|
||||
isSelected={selectedTabId === TabId.Status}
|
||||
onClick={() => setSelectedTabId(TabId.Status)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.engineStatus.tabTitle"
|
||||
defaultMessage="Engine Status"
|
||||
/>
|
||||
</EuiTab>
|
||||
)}
|
||||
</EuiTabs>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="xl">
|
||||
<FileUploadSection
|
||||
assetCriticalityPrivilegesError={assetCriticalityPrivilegesError}
|
||||
hasEntityAnalyticsCapability={hasEntityAnalyticsCapability}
|
||||
hasAssetCriticalityWritePermissions={hasAssetCriticalityWritePermissions}
|
||||
/>
|
||||
{selectedTabId === TabId.Import && (
|
||||
<FileUploadSection
|
||||
assetCriticalityPrivilegesError={assetCriticalityPrivilegesError}
|
||||
hasEntityAnalyticsCapability={hasEntityAnalyticsCapability}
|
||||
hasAssetCriticalityWritePermissions={hasAssetCriticalityWritePermissions}
|
||||
/>
|
||||
)}
|
||||
{selectedTabId === TabId.Status && <EngineStatus />}
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiFlexGroup direction="column">
|
||||
{enableStoreMutation.isError && (
|
||||
|
@ -210,19 +268,7 @@ export const EntityStoreManagementPage = () => {
|
|||
</EuiCallOut>
|
||||
)}
|
||||
{callouts}
|
||||
<WhatIsAssetCriticalityPanel />
|
||||
{!isEntityStoreFeatureFlagDisabled &&
|
||||
privileges?.has_all_required &&
|
||||
canDeleteEntityEngine(entityStoreStatus.data?.status) && (
|
||||
<ClearEntityDataPanel
|
||||
{...{
|
||||
deleteEntityEngineMutation,
|
||||
isClearModalVisible,
|
||||
closeClearModal,
|
||||
showClearModal,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{selectedTabId === TabId.Import && <WhatIsAssetCriticalityPanel />}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -375,10 +421,8 @@ const InsufficientAssetCriticalityPrivilegesCallout: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const AssetCriticalityIssueCallout: React.FC = ({
|
||||
const AssetCriticalityIssueCallout: React.FC<{ errorMessage?: string | ReactNode }> = ({
|
||||
errorMessage,
|
||||
}: {
|
||||
errorMessage?: string | ReactNode;
|
||||
}) => {
|
||||
const msg = errorMessage ?? (
|
||||
<FormattedMessage
|
||||
|
@ -405,7 +449,7 @@ const AssetCriticalityIssueCallout: React.FC = ({
|
|||
);
|
||||
};
|
||||
|
||||
const ClearEntityDataPanel: React.FC<{
|
||||
const ClearEntityDataButton: React.FC<{
|
||||
deleteEntityEngineMutation: ReturnType<typeof useDeleteEntityEngineMutation>;
|
||||
isClearModalVisible: boolean;
|
||||
closeClearModal: () => void;
|
||||
|
@ -413,37 +457,19 @@ const ClearEntityDataPanel: React.FC<{
|
|||
}> = ({ deleteEntityEngineMutation, isClearModalVisible, closeClearModal, showClearModal }) => {
|
||||
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>
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={() => {
|
||||
showClearModal();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clear"
|
||||
defaultMessage="Clear Entity Data"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
{isClearModalVisible && (
|
||||
<EuiConfirmModal
|
||||
isLoading={deleteEntityEngineMutation.isLoading}
|
||||
|
@ -494,21 +520,15 @@ const FileUploadSection: React.FC<{
|
|||
hasAssetCriticalityWritePermissions,
|
||||
}) => {
|
||||
if (!hasEntityAnalyticsCapability || assetCriticalityPrivilegesError?.body.status_code === 403) {
|
||||
return <AssetCriticalityIssueCallout />;
|
||||
return (
|
||||
<AssetCriticalityIssueCallout errorMessage={assetCriticalityPrivilegesError?.body.message} />
|
||||
);
|
||||
}
|
||||
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
|
||||
|
|
|
@ -1,18 +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.
|
||||
*/
|
||||
|
||||
export const EntityStoreResource = {
|
||||
ENTITY_ENGINE: 'entity_engine',
|
||||
ENTITY_DEFINITION: 'entity_definition',
|
||||
ENTITY_INDEX: 'entity_index',
|
||||
INDEX_COMPONENT_TEMPLATE: 'index_component_template',
|
||||
PLATFORM_PIPELINE: 'platform_pipeline',
|
||||
FIELD_RETENTION_ENRICH_POLICY: 'field_retention_enrich_policy',
|
||||
FIELD_RETENTION_ENRICH_POLICY_TASK: 'field_retention_enrich_policy_task',
|
||||
} as const;
|
||||
|
||||
export type EntityStoreResource = (typeof EntityStoreResource)[keyof typeof EntityStoreResource];
|
|
@ -6,6 +6,10 @@
|
|||
*/
|
||||
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import {
|
||||
EngineComponentResourceEnum,
|
||||
type EngineComponentStatus,
|
||||
} from '../../../../../common/api/entity_analytics';
|
||||
import type { UnitedEntityDefinition } from '../united_entity_definitions';
|
||||
|
||||
const getComponentTemplateName = (definitionId: string) => `${definitionId}-latest@platform`;
|
||||
|
@ -41,3 +45,24 @@ export const deleteEntityIndexComponentTemplate = ({ unitedDefinition, esClient
|
|||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getEntityIndexComponentTemplateStatus = async ({
|
||||
definitionId,
|
||||
esClient,
|
||||
}: Pick<Options, 'esClient'> & { definitionId: string }): Promise<EngineComponentStatus> => {
|
||||
const name = getComponentTemplateName(definitionId);
|
||||
const componentTemplate = await esClient.cluster.getComponentTemplate(
|
||||
{
|
||||
name,
|
||||
},
|
||||
{
|
||||
ignore: [404],
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id: name,
|
||||
installed: componentTemplate?.component_templates?.length > 0,
|
||||
resource: EngineComponentResourceEnum.component_template,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
import type { EnrichPutPolicyRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { EngineComponentResourceEnum } from '../../../../../common/api/entity_analytics';
|
||||
import { getEntitiesIndexName } from '../utils';
|
||||
import type { UnitedEntityDefinition } from '../united_entity_definitions';
|
||||
|
||||
|
@ -105,3 +106,21 @@ export const deleteFieldRetentionEnrichPolicy = async ({
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getFieldRetentionEnrichPolicyStatus = async ({
|
||||
definitionMetadata,
|
||||
esClient,
|
||||
}: {
|
||||
definitionMetadata: DefinitionMetadata;
|
||||
esClient: ElasticsearchClient;
|
||||
}) => {
|
||||
const name = getFieldRetentionEnrichPolicyName(definitionMetadata);
|
||||
const policy = await esClient.enrich.getPolicy({ name }, { ignore: [404] });
|
||||
const policies = policy.policies;
|
||||
|
||||
return {
|
||||
installed: policies.length > 0,
|
||||
id: name,
|
||||
resource: EngineComponentResourceEnum.enrich_policy,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
*/
|
||||
|
||||
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
import type { EntityType } from '../../../../../common/api/entity_analytics';
|
||||
import {
|
||||
EngineComponentResourceEnum,
|
||||
type EngineComponentStatus,
|
||||
type EntityType,
|
||||
} from '../../../../../common/api/entity_analytics';
|
||||
import { getEntitiesIndexName } from '../utils';
|
||||
import { createOrUpdateIndex } from '../../utils/create_or_update_index';
|
||||
|
||||
|
@ -36,3 +40,21 @@ export const deleteEntityIndex = ({ entityType, esClient, namespace }: Options)
|
|||
ignore: [404],
|
||||
}
|
||||
);
|
||||
|
||||
export const getEntityIndexStatus = async ({
|
||||
entityType,
|
||||
esClient,
|
||||
namespace,
|
||||
}: Pick<Options, 'entityType' | 'namespace' | 'esClient'>): Promise<EngineComponentStatus> => {
|
||||
const index = getEntitiesIndexName(entityType, namespace);
|
||||
const exists = await esClient.indices.exists(
|
||||
{
|
||||
index,
|
||||
},
|
||||
{
|
||||
ignore: [404],
|
||||
}
|
||||
);
|
||||
|
||||
return { id: index, installed: exists, resource: EngineComponentResourceEnum.index };
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import type { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
import type { IngestProcessorContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { EntityDefinition } from '@kbn/entities-schema';
|
||||
import { EngineComponentResourceEnum } from '../../../../../common/api/entity_analytics';
|
||||
import { type FieldRetentionDefinition } from '../field_retention_definition';
|
||||
import {
|
||||
debugDeepCopyContextStep,
|
||||
|
@ -161,3 +162,27 @@ export const deletePlatformPipeline = ({
|
|||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getPlatformPipelineStatus = async ({
|
||||
definition,
|
||||
esClient,
|
||||
}: {
|
||||
definition: EntityDefinition;
|
||||
esClient: ElasticsearchClient;
|
||||
}) => {
|
||||
const pipelineId = getPlatformPipelineId(definition);
|
||||
const pipeline = await esClient.ingest.getPipeline(
|
||||
{
|
||||
id: pipelineId,
|
||||
},
|
||||
{
|
||||
ignore: [404],
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id: pipelineId,
|
||||
installed: !!pipeline[pipelineId],
|
||||
resource: EngineComponentResourceEnum.ingest_pipeline,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -20,19 +20,28 @@ import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
|
|||
import type { DataViewsService } from '@kbn/data-views-plugin/common';
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import moment from 'moment';
|
||||
import type { EntityDefinitionWithState } from '@kbn/entityManager-plugin/server/lib/entities/types';
|
||||
import type { EntityDefinition } from '@kbn/entities-schema';
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import type {
|
||||
GetEntityStoreStatusRequestQuery,
|
||||
GetEntityStoreStatusResponse,
|
||||
} from '../../../../common/api/entity_analytics/entity_store/status.gen';
|
||||
import type {
|
||||
InitEntityStoreRequestBody,
|
||||
InitEntityStoreResponse,
|
||||
} from '../../../../common/api/entity_analytics/entity_store/enablement.gen';
|
||||
} from '../../../../common/api/entity_analytics/entity_store/enable.gen';
|
||||
import type { AppClient } from '../../..';
|
||||
import { EntityType } from '../../../../common/api/entity_analytics';
|
||||
import { EngineComponentResourceEnum, EntityType } from '../../../../common/api/entity_analytics';
|
||||
import type {
|
||||
Entity,
|
||||
EngineDataviewUpdateResult,
|
||||
InitEntityEngineRequestBody,
|
||||
InitEntityEngineResponse,
|
||||
InspectQuery,
|
||||
ListEntityEnginesResponse,
|
||||
EngineComponentStatus,
|
||||
EngineComponentResource,
|
||||
} from '../../../../common/api/entity_analytics';
|
||||
import { EngineDescriptorClient } from './saved_object/engine_descriptor';
|
||||
import { ENGINE_STATUS, ENTITY_STORE_STATUS, MAX_SEARCH_RESPONSE_SIZE } from './constants';
|
||||
|
@ -41,6 +50,7 @@ import { getUnitedEntityDefinition } from './united_entity_definitions';
|
|||
import {
|
||||
startEntityStoreFieldRetentionEnrichTask,
|
||||
removeEntityStoreFieldRetentionEnrichTask,
|
||||
getEntityStoreFieldRetentionEnrichTaskState as getEntityStoreFieldRetentionEnrichTaskStatus,
|
||||
} from './task';
|
||||
import {
|
||||
createEntityIndex,
|
||||
|
@ -52,6 +62,10 @@ import {
|
|||
createFieldRetentionEnrichPolicy,
|
||||
executeFieldRetentionEnrichPolicy,
|
||||
deleteFieldRetentionEnrichPolicy,
|
||||
getPlatformPipelineStatus,
|
||||
getFieldRetentionEnrichPolicyStatus,
|
||||
getEntityIndexStatus,
|
||||
getEntityIndexComponentTemplateStatus,
|
||||
} from './elasticsearch_assets';
|
||||
import { RiskScoreDataClient } from '../risk_score/risk_score_data_client';
|
||||
import {
|
||||
|
@ -62,7 +76,6 @@ import {
|
|||
isPromiseRejected,
|
||||
} from './utils';
|
||||
import { EntityEngineActions } from './auditing/actions';
|
||||
import { EntityStoreResource } from './auditing/resources';
|
||||
import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../audit';
|
||||
import type { EntityRecord, EntityStoreConfig } from './types';
|
||||
import {
|
||||
|
@ -71,6 +84,19 @@ import {
|
|||
} from '../../telemetry/event_based/events';
|
||||
import { CRITICALITY_VALUES } from '../asset_criticality/constants';
|
||||
|
||||
// Workaround. TransformState type is wrong. The health type should be: TransformHealth from '@kbn/transform-plugin/common/types/transform_stats'
|
||||
export interface TransformHealth extends estypes.TransformGetTransformStatsTransformStatsHealth {
|
||||
issues?: TransformHealthIssue[];
|
||||
}
|
||||
|
||||
export interface TransformHealthIssue {
|
||||
type: string;
|
||||
issue: string;
|
||||
details?: string;
|
||||
count: number;
|
||||
first_occurrence?: number;
|
||||
}
|
||||
|
||||
interface EntityStoreClientOpts {
|
||||
logger: Logger;
|
||||
clusterClient: IScopedClusterClient;
|
||||
|
@ -131,6 +157,42 @@ export class EntityStoreDataClient {
|
|||
});
|
||||
}
|
||||
|
||||
private async getEngineComponentsState(
|
||||
type: EntityType,
|
||||
definition?: EntityDefinition
|
||||
): Promise<EngineComponentStatus[]> {
|
||||
const { namespace, taskManager } = this.options;
|
||||
|
||||
return definition
|
||||
? Promise.all([
|
||||
...(taskManager
|
||||
? [getEntityStoreFieldRetentionEnrichTaskStatus({ namespace, taskManager })]
|
||||
: []),
|
||||
getPlatformPipelineStatus({
|
||||
definition,
|
||||
esClient: this.esClient,
|
||||
}),
|
||||
getFieldRetentionEnrichPolicyStatus({
|
||||
definitionMetadata: {
|
||||
namespace,
|
||||
entityType: type,
|
||||
version: definition.version,
|
||||
},
|
||||
esClient: this.esClient,
|
||||
}),
|
||||
getEntityIndexStatus({
|
||||
entityType: type,
|
||||
esClient: this.esClient,
|
||||
namespace,
|
||||
}),
|
||||
getEntityIndexComponentTemplateStatus({
|
||||
definitionId: definition.id,
|
||||
esClient: this.esClient,
|
||||
}),
|
||||
])
|
||||
: Promise.resolve([] as EngineComponentStatus[]);
|
||||
}
|
||||
|
||||
public async enable(
|
||||
{ indexPattern = '', filter = '', fieldHistoryLength = 10 }: InitEntityStoreRequestBody,
|
||||
{ pipelineDebugMode = false }: { pipelineDebugMode?: boolean } = {}
|
||||
|
@ -152,7 +214,10 @@ export class EntityStoreDataClient {
|
|||
return { engines, succeeded: true };
|
||||
}
|
||||
|
||||
public async status(): Promise<GetEntityStoreStatusResponse> {
|
||||
public async status({
|
||||
include_components: withComponents = false,
|
||||
}: GetEntityStoreStatusRequestQuery): Promise<GetEntityStoreStatusResponse> {
|
||||
const { namespace } = this.options;
|
||||
const { engines, count } = await this.engineClient.list();
|
||||
|
||||
let status = ENTITY_STORE_STATUS.RUNNING;
|
||||
|
@ -166,7 +231,38 @@ export class EntityStoreDataClient {
|
|||
status = ENTITY_STORE_STATUS.INSTALLING;
|
||||
}
|
||||
|
||||
return { engines, status };
|
||||
if (withComponents) {
|
||||
const enginesWithComponents = await Promise.all(
|
||||
engines.map(async (engine) => {
|
||||
const entityDefinitionId = buildEntityDefinitionId(engine.type, namespace);
|
||||
const {
|
||||
definitions: [definition],
|
||||
} = await this.entityClient.getEntityDefinitions({
|
||||
id: entityDefinitionId,
|
||||
includeState: withComponents,
|
||||
});
|
||||
|
||||
const definitionComponents = this.getComponentFromEntityDefinition(
|
||||
entityDefinitionId,
|
||||
definition
|
||||
);
|
||||
|
||||
const entityStoreComponents = await this.getEngineComponentsState(
|
||||
engine.type,
|
||||
definition
|
||||
);
|
||||
|
||||
return {
|
||||
...engine,
|
||||
components: [...definitionComponents, ...entityStoreComponents],
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return { engines: enginesWithComponents, status };
|
||||
} else {
|
||||
return { engines, status };
|
||||
}
|
||||
}
|
||||
|
||||
public async init(
|
||||
|
@ -203,7 +299,7 @@ export class EntityStoreDataClient {
|
|||
this.log('info', entityType, `Initializing entity store`);
|
||||
this.audit(
|
||||
EntityEngineActions.INIT,
|
||||
EntityStoreResource.ENTITY_ENGINE,
|
||||
EngineComponentResourceEnum.entity_engine,
|
||||
entityType,
|
||||
'Initializing entity engine'
|
||||
);
|
||||
|
@ -332,7 +428,7 @@ export class EntityStoreDataClient {
|
|||
|
||||
this.audit(
|
||||
EntityEngineActions.INIT,
|
||||
EntityStoreResource.ENTITY_ENGINE,
|
||||
EngineComponentResourceEnum.entity_engine,
|
||||
entityType,
|
||||
'Failed to initialize entity engine resources',
|
||||
err
|
||||
|
@ -355,6 +451,50 @@ export class EntityStoreDataClient {
|
|||
}
|
||||
}
|
||||
|
||||
public getComponentFromEntityDefinition(
|
||||
id: string,
|
||||
definition: EntityDefinitionWithState | EntityDefinition
|
||||
): EngineComponentStatus[] {
|
||||
if (!definition) {
|
||||
return [
|
||||
{
|
||||
id,
|
||||
installed: false,
|
||||
resource: EngineComponentResourceEnum.entity_definition,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if ('state' in definition) {
|
||||
return [
|
||||
{
|
||||
id: definition.id,
|
||||
installed: definition.state.installed,
|
||||
resource: EngineComponentResourceEnum.entity_definition,
|
||||
},
|
||||
...definition.state.components.transforms.map(({ installed, running, stats }) => ({
|
||||
id,
|
||||
resource: EngineComponentResourceEnum.transform,
|
||||
installed,
|
||||
errors: (stats?.health as TransformHealth)?.issues?.map(({ issue, details }) => ({
|
||||
title: issue,
|
||||
message: details,
|
||||
})),
|
||||
})),
|
||||
...definition.state.components.ingestPipelines.map((pipeline) => ({
|
||||
resource: EngineComponentResourceEnum.ingest_pipeline,
|
||||
...pipeline,
|
||||
})),
|
||||
...definition.state.components.indexTemplates.map(({ installed }) => ({
|
||||
id,
|
||||
installed,
|
||||
resource: EngineComponentResourceEnum.index_template,
|
||||
})),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public async getExistingEntityDefinition(entityType: EntityType) {
|
||||
const entityDefinitionId = buildEntityDefinitionId(entityType, this.options.namespace);
|
||||
|
||||
|
@ -387,7 +527,7 @@ export class EntityStoreDataClient {
|
|||
const fullEntityDefinition = await this.getExistingEntityDefinition(entityType);
|
||||
this.audit(
|
||||
EntityEngineActions.START,
|
||||
EntityStoreResource.ENTITY_DEFINITION,
|
||||
EngineComponentResourceEnum.entity_definition,
|
||||
entityType,
|
||||
'Starting entity definition'
|
||||
);
|
||||
|
@ -414,7 +554,7 @@ export class EntityStoreDataClient {
|
|||
const fullEntityDefinition = await this.getExistingEntityDefinition(entityType);
|
||||
this.audit(
|
||||
EntityEngineActions.STOP,
|
||||
EntityStoreResource.ENTITY_DEFINITION,
|
||||
EngineComponentResourceEnum.entity_definition,
|
||||
entityType,
|
||||
'Stopping entity definition'
|
||||
);
|
||||
|
@ -428,7 +568,7 @@ export class EntityStoreDataClient {
|
|||
return this.engineClient.get(entityType);
|
||||
}
|
||||
|
||||
public async list() {
|
||||
public async list(): Promise<ListEntityEnginesResponse> {
|
||||
return this.engineClient.list();
|
||||
}
|
||||
|
||||
|
@ -457,7 +597,7 @@ export class EntityStoreDataClient {
|
|||
this.log('info', entityType, `Deleting entity store`);
|
||||
this.audit(
|
||||
EntityEngineActions.DELETE,
|
||||
EntityStoreResource.ENTITY_ENGINE,
|
||||
EngineComponentResourceEnum.entity_engine,
|
||||
entityType,
|
||||
'Deleting entity engine'
|
||||
);
|
||||
|
@ -525,7 +665,7 @@ export class EntityStoreDataClient {
|
|||
|
||||
this.audit(
|
||||
EntityEngineActions.DELETE,
|
||||
EntityStoreResource.ENTITY_ENGINE,
|
||||
EngineComponentResourceEnum.entity_engine,
|
||||
entityType,
|
||||
'Failed to delete entity engine',
|
||||
err
|
||||
|
@ -672,7 +812,7 @@ export class EntityStoreDataClient {
|
|||
|
||||
private audit(
|
||||
action: EntityEngineActions,
|
||||
resource: EntityStoreResource,
|
||||
resource: EngineComponentResource,
|
||||
entityType: EntityType,
|
||||
msg: string,
|
||||
error?: Error
|
||||
|
|
|
@ -10,8 +10,8 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
|||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
||||
|
||||
import type { InitEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/enablement.gen';
|
||||
import { InitEntityStoreRequestBody } from '../../../../../common/api/entity_analytics/entity_store/enablement.gen';
|
||||
import type { InitEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/enable.gen';
|
||||
import { InitEntityStoreRequestBody } from '../../../../../common/api/entity_analytics/entity_store/enable.gen';
|
||||
import { API_VERSIONS, APP_ID } from '../../../../../common/constants';
|
||||
import type { EntityAnalyticsRoutesDeps } from '../../types';
|
||||
import { checkAndInitAssetCriticalityResources } from '../../asset_criticality/check_and_init_asset_criticality_resources';
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IKibanaResponse, Logger } from '@kbn/core/server';
|
||||
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
||||
|
||||
import type { GetEntityEngineStatsResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/stats.gen';
|
||||
import { GetEntityEngineStatsRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/stats.gen';
|
||||
import { API_VERSIONS, APP_ID } from '../../../../../common/constants';
|
||||
import type { EntityAnalyticsRoutesDeps } from '../../types';
|
||||
|
||||
export const getEntityEngineStatsRoute = (
|
||||
router: EntityAnalyticsRoutesDeps['router'],
|
||||
logger: Logger
|
||||
) => {
|
||||
router.versioned
|
||||
.post({
|
||||
access: 'public',
|
||||
path: '/api/entity_store/engines/{entityType}/stats',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {
|
||||
request: {
|
||||
params: buildRouteValidationWithZod(GetEntityEngineStatsRequestParams),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
async (
|
||||
context,
|
||||
request,
|
||||
response
|
||||
): Promise<IKibanaResponse<GetEntityEngineStatsResponse>> => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
|
||||
try {
|
||||
// TODO
|
||||
throw new Error('Not implemented');
|
||||
|
||||
// return response.ok({ body });
|
||||
} catch (e) {
|
||||
logger.error('Error in GetEntityEngineStats:', e);
|
||||
const error = transformError(e);
|
||||
return siemResponse.error({
|
||||
statusCode: error.statusCode,
|
||||
body: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -15,7 +15,9 @@
|
|||
import type { IKibanaResponse, Logger } from '@kbn/core/server';
|
||||
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/enablement.gen';
|
||||
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
||||
import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/status.gen';
|
||||
import { GetEntityStoreStatusRequestQuery } from '../../../../../common/api/entity_analytics/entity_store/status.gen';
|
||||
import { API_VERSIONS, APP_ID } from '../../../../../common/constants';
|
||||
import type { EntityAnalyticsRoutesDeps } from '../../types';
|
||||
import { checkAndInitAssetCriticalityResources } from '../../asset_criticality/check_and_init_asset_criticality_resources';
|
||||
|
@ -38,7 +40,11 @@ export const getEntityStoreStatusRoute = (
|
|||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {},
|
||||
validate: {
|
||||
request: {
|
||||
query: buildRouteValidationWithZod(GetEntityStoreStatusRequestQuery),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
async (
|
||||
|
@ -54,7 +60,7 @@ export const getEntityStoreStatusRoute = (
|
|||
try {
|
||||
const body: GetEntityStoreStatusResponse = await secSol
|
||||
.getEntityStoreDataClient()
|
||||
.status();
|
||||
.status(request.query);
|
||||
|
||||
return response.ok({ body });
|
||||
} catch (e) {
|
||||
|
|
|
@ -13,7 +13,10 @@ import type {
|
|||
TaskManagerSetupContract,
|
||||
TaskManagerStartContract,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import type { EntityType } from '../../../../../common/api/entity_analytics/entity_store';
|
||||
import {
|
||||
EngineComponentResourceEnum,
|
||||
type EntityType,
|
||||
} from '../../../../../common/api/entity_analytics/entity_store';
|
||||
import {
|
||||
defaultState,
|
||||
stateSchemaByVersion,
|
||||
|
@ -275,3 +278,37 @@ const createTaskRunnerFactory =
|
|||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getEntityStoreFieldRetentionEnrichTaskState = async ({
|
||||
namespace,
|
||||
taskManager,
|
||||
}: {
|
||||
namespace: string;
|
||||
taskManager: TaskManagerStartContract;
|
||||
}) => {
|
||||
const taskId = getTaskId(namespace);
|
||||
try {
|
||||
const taskState = await taskManager.get(taskId);
|
||||
|
||||
return {
|
||||
id: taskState.id,
|
||||
resource: EngineComponentResourceEnum.task,
|
||||
installed: true,
|
||||
enabled: taskState.enabled,
|
||||
status: taskState.status,
|
||||
retryAttempts: taskState.attempts,
|
||||
nextRun: taskState.runAt,
|
||||
lastRun: taskState.state.lastExecutionTimestamp,
|
||||
runs: taskState.state.runs,
|
||||
};
|
||||
} catch (e) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
|
||||
return {
|
||||
id: taskId,
|
||||
installed: false,
|
||||
resource: EngineComponentResourceEnum.task,
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -40397,7 +40397,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.noPermissionTitle": "Privilèges d'index insuffisants pour effectuer le chargement de fichiers CSV",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.resultsStepTitle": "Résultats",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.selectFileStepTitle": "Sélectionner un fichier",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.subTitle": "Importer des entités à l'aide d'un fichier texte",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unavailable": "La fonctionnalité de chargement de fichiers CSV de criticité des ressources n'est pas disponible.",
|
||||
"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}",
|
||||
|
@ -40439,7 +40438,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.clearAllEntities": "Effacer toutes les entités",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.close": "Fermer",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.title": "Effacer les données des entités ?",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntityData": "Supprimez toutes les données d'entité extraites du stockage. Cette action supprimera définitivement les enregistrements d’utilisateur et d'hôte persistants, les données ne seront par ailleurs plus disponibles pour l'analyse. Procédez avec prudence, car cette opération est irréversible. Notez que cette opération ne supprimera pas les données sources, les scores de risque des entités ni les attributions de criticité des ressources.",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.featureFlagDisabled": "Les fonctionnalités du stockage d'entités ne sont pas disponibles",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.subTitle": "Permet un monitoring complet des hôtes et des utilisateurs de votre système.",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.title": "Stockage d'entités",
|
||||
|
|
|
@ -40366,7 +40366,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.noPermissionTitle": "CSVアップロードを実行する十分なインデックス権限がありません",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.resultsStepTitle": "結果",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.selectFileStepTitle": "ファイルを選択",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.subTitle": "テキストファイルを使用してエンティティをインポート",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unavailable": "アセット重要度CSVファイルアップロード機能が利用できません。",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unsupportedFileTypeError": "無効なファイル形式が選択されました。{supportedFileExtensions}ファイルを選択して再試行してください。",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.uploadFileSizeLimit": "最大ファイルサイズ:{maxFileSize}",
|
||||
|
@ -40408,7 +40407,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.clearAllEntities": "すべてのエンティティを消去",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.close": "閉じる",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.title": "エンティティデータを消去しますか?",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntityData": "すべての抽出されたエンティティデータをストアから削除します。このアクションにより、永続化されたユーザーおよびホストレコードが完全に削除され、データは分析で使用できなくなります。注意して続行してください。この操作は元に戻せません。この処理では、ソースデータ、エンティティリスクスコア、アセット重要度割り当ては削除されません。",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.featureFlagDisabled": "エンティティストア機能を利用できません",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.subTitle": "システムのホストとユーザーを包括的に監視できます。",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.title": "エンティティストア",
|
||||
|
|
|
@ -39738,7 +39738,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.noPermissionTitle": "索引权限不足,无法执行 CSV 上传",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.resultsStepTitle": "结果",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.selectFileStepTitle": "选择文件",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.subTitle": "使用文本文件导入实体",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unavailable": "资产关键度 CSV 文件上传功能不可用。",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.unsupportedFileTypeError": "选定的文件格式无效。请选择 {supportedFileExtensions} 文件,然后重试",
|
||||
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.uploadFileSizeLimit": "最大文件大小:{maxFileSize}",
|
||||
|
@ -39780,7 +39779,6 @@
|
|||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.clearAllEntities": "清除所有实体",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.close": "关闭",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntitiesModal.title": "清除实体数据?",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.clearEntityData": "移除从仓库中提取的所有实体数据。此操作将永久删除持久化用户和主机记录,并且数据将不再可用于分析。请谨慎操作,因为此操作无法撤消。请注意,此操作不会删除源数据、实体风险分数或资产关键度分配。",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.featureFlagDisabled": "实体仓库功能不可用",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.subTitle": "允许全面监测您系统的主机和用户。",
|
||||
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.title": "实体仓库",
|
||||
|
|
|
@ -80,7 +80,7 @@ import {
|
|||
GetEndpointSuggestionsRequestBodyInput,
|
||||
} from '@kbn/security-solution-plugin/common/api/endpoint/suggestions/get_suggestions.gen';
|
||||
import { GetEntityEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/get.gen';
|
||||
import { GetEntityEngineStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/stats.gen';
|
||||
import { GetEntityStoreStatusRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/status.gen';
|
||||
import { GetNotesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_notes/get_notes_route.gen';
|
||||
import { GetPolicyResponseRequestQueryInput } from '@kbn/security-solution-plugin/common/api/endpoint/policy/policy_response.gen';
|
||||
import { GetProtectionUpdatesNoteRequestParamsInput } from '@kbn/security-solution-plugin/common/api/endpoint/protection_updates_note/protection_updates_note.gen';
|
||||
|
@ -106,7 +106,7 @@ import {
|
|||
InitEntityEngineRequestParamsInput,
|
||||
InitEntityEngineRequestBodyInput,
|
||||
} from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/init.gen';
|
||||
import { InitEntityStoreRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/enablement.gen';
|
||||
import { InitEntityStoreRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/enable.gen';
|
||||
import { InstallPrepackedTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route.gen';
|
||||
import { ListEntitiesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/entities/list_entities.gen';
|
||||
import { PatchRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/patch_rule/patch_rule_route.gen';
|
||||
|
@ -832,24 +832,13 @@ finalize it.
|
|||
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
|
||||
},
|
||||
getEntityEngineStats(props: GetEntityEngineStatsProps, kibanaSpace: string = 'default') {
|
||||
return supertest
|
||||
.post(
|
||||
routeWithNamespace(
|
||||
replaceParams('/api/entity_store/engines/{entityType}/stats', props.params),
|
||||
kibanaSpace
|
||||
)
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
|
||||
},
|
||||
getEntityStoreStatus(kibanaSpace: string = 'default') {
|
||||
getEntityStoreStatus(props: GetEntityStoreStatusProps, kibanaSpace: string = 'default') {
|
||||
return supertest
|
||||
.get(routeWithNamespace('/api/entity_store/status', kibanaSpace))
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.query(props.query);
|
||||
},
|
||||
/**
|
||||
* Get all notes for a given document.
|
||||
|
@ -1615,8 +1604,8 @@ export interface GetEndpointSuggestionsProps {
|
|||
export interface GetEntityEngineProps {
|
||||
params: GetEntityEngineRequestParamsInput;
|
||||
}
|
||||
export interface GetEntityEngineStatsProps {
|
||||
params: GetEntityEngineStatsRequestParamsInput;
|
||||
export interface GetEntityStoreStatusProps {
|
||||
query: GetEntityStoreStatusRequestQueryInput;
|
||||
}
|
||||
export interface GetNotesProps {
|
||||
query: GetNotesRequestQueryInput;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import expect from 'expect';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
import { EntityStoreUtils } from '../../utils';
|
||||
import { dataViewRouteHelpersFactory } from '../../utils/data_view';
|
||||
|
@ -14,8 +14,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const supertest = getService('supertest');
|
||||
|
||||
const utils = EntityStoreUtils(getService);
|
||||
// Failing: See https://github.com/elastic/kibana/issues/200758
|
||||
describe.skip('@ess @skipInServerlessMKI Entity Store APIs', () => {
|
||||
describe('@ess @skipInServerlessMKI Entity Store APIs', () => {
|
||||
const dataView = dataViewRouteHelpersFactory(supertest);
|
||||
|
||||
before(async () => {
|
||||
|
@ -85,7 +84,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
expect(getResponse.body).to.eql({
|
||||
expect(getResponse.body).toEqual({
|
||||
status: 'started',
|
||||
type: 'host',
|
||||
indexPattern: '',
|
||||
|
@ -101,7 +100,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
expect(getResponse.body).to.eql({
|
||||
expect(getResponse.body).toEqual({
|
||||
status: 'started',
|
||||
type: 'user',
|
||||
indexPattern: '',
|
||||
|
@ -118,7 +117,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
// @ts-expect-error body is any
|
||||
const sortedEngines = body.engines.sort((a, b) => a.type.localeCompare(b.type));
|
||||
|
||||
expect(sortedEngines).to.eql([
|
||||
expect(sortedEngines).toEqual([
|
||||
{
|
||||
status: 'started',
|
||||
type: 'host',
|
||||
|
@ -160,7 +159,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body.status).to.eql('stopped');
|
||||
expect(body.status).toEqual('stopped');
|
||||
});
|
||||
|
||||
it('should start the entity engine', async () => {
|
||||
|
@ -176,7 +175,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body.status).to.eql('started');
|
||||
expect(body.status).toEqual('started');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -213,10 +212,11 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
afterEach(async () => {
|
||||
await utils.cleanEngines();
|
||||
});
|
||||
it('should return "not_installed" when no engines have been initialized', async () => {
|
||||
const { body } = await api.getEntityStoreStatus().expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
it('should return "not_installed" when no engines have been initialized', async () => {
|
||||
const { body } = await api.getEntityStoreStatus({ query: {} }).expect(200);
|
||||
|
||||
expect(body).toEqual({
|
||||
engines: [],
|
||||
status: 'not_installed',
|
||||
});
|
||||
|
@ -225,23 +225,56 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
it('should return "installing" when at least one engine is being initialized', async () => {
|
||||
await utils.enableEntityStore();
|
||||
|
||||
const { body } = await api.getEntityStoreStatus().expect(200);
|
||||
const { body } = await api.getEntityStoreStatus({ query: {} }).expect(200);
|
||||
|
||||
expect(body.status).to.eql('installing');
|
||||
expect(body.engines.length).to.eql(2);
|
||||
expect(body.engines[0].status).to.eql('installing');
|
||||
expect(body.engines[1].status).to.eql('installing');
|
||||
expect(body.status).toEqual('installing');
|
||||
expect(body.engines.length).toEqual(2);
|
||||
expect(body.engines[0].status).toEqual('installing');
|
||||
expect(body.engines[1].status).toEqual('installing');
|
||||
});
|
||||
|
||||
it('should return "started" when all engines are started', async () => {
|
||||
await utils.initEntityEngineForEntityTypesAndWait(['host', 'user']);
|
||||
|
||||
const { body } = await api.getEntityStoreStatus().expect(200);
|
||||
const { body } = await api.getEntityStoreStatus({ query: {} }).expect(200);
|
||||
|
||||
expect(body.status).to.eql('running');
|
||||
expect(body.engines.length).to.eql(2);
|
||||
expect(body.engines[0].status).to.eql('started');
|
||||
expect(body.engines[1].status).to.eql('started');
|
||||
expect(body.status).toEqual('running');
|
||||
expect(body.engines.length).toEqual(2);
|
||||
expect(body.engines[0].status).toEqual('started');
|
||||
expect(body.engines[1].status).toEqual('started');
|
||||
});
|
||||
|
||||
describe('status with components', () => {
|
||||
it('should return empty list when when no engines have been initialized', async () => {
|
||||
const { body } = await api
|
||||
.getEntityStoreStatus({ query: { include_components: true } })
|
||||
.expect(200);
|
||||
|
||||
expect(body).toEqual({
|
||||
engines: [],
|
||||
status: 'not_installed',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return components status when engines are installed', async () => {
|
||||
await utils.initEntityEngineForEntityTypesAndWait(['host']);
|
||||
|
||||
const { body } = await api
|
||||
.getEntityStoreStatus({ query: { include_components: true } })
|
||||
.expect(200);
|
||||
|
||||
expect(body.engines[0].components).toEqual([
|
||||
expect.objectContaining({ resource: 'entity_definition' }),
|
||||
expect.objectContaining({ resource: 'transform' }),
|
||||
expect.objectContaining({ resource: 'ingest_pipeline' }),
|
||||
expect.objectContaining({ resource: 'index_template' }),
|
||||
expect.objectContaining({ resource: 'task' }),
|
||||
expect.objectContaining({ resource: 'ingest_pipeline' }),
|
||||
expect.objectContaining({ resource: 'enrich_policy' }),
|
||||
expect.objectContaining({ resource: 'index' }),
|
||||
expect.objectContaining({ resource: 'component_template' }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -262,14 +295,14 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
it("should not update the index patten when it didn't change", async () => {
|
||||
const response = await api.applyEntityEngineDataviewIndices();
|
||||
|
||||
expect(response.body).to.eql({ success: true, result: [{ type: 'host', changes: {} }] });
|
||||
expect(response.body).toEqual({ success: true, result: [{ type: 'host', changes: {} }] });
|
||||
});
|
||||
|
||||
it('should update the index pattern when the data view changes', async () => {
|
||||
await dataView.updateIndexPattern('security-solution', 'test-*');
|
||||
const response = await api.applyEntityEngineDataviewIndices();
|
||||
|
||||
expect(response.body).to.eql({
|
||||
expect(response.body).toEqual({
|
||||
success: true,
|
||||
result: [
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue