mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Entity Analytics][UI] UI changes for Risk Engine to include closed alerts for risk score calculation (#201909)
## Summary We are introducing a new feature that allows users to include "closed" alerts in risk score calculations. Users can toggle a button to include closed alerts in the risk score calculation and specify a date/time range for the calculation. Additionally, they can preview the data before finalising and saving these changes for the next engine run.  ### **Note : This PR is an extension to the following PRs.** - [API] : https://github.com/elastic/kibana/pull/201344 - [API] : https://github.com/elastic/kibana/pull/201397 ### Checklist Check the PR satisfies following conditions. 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] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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 - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] 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
a9f076cb1f
commit
a95ec61444
39 changed files with 1615 additions and 164 deletions
|
@ -33545,6 +33545,58 @@ paths:
|
|||
tags:
|
||||
- Security Entity Analytics API
|
||||
x-beta: true
|
||||
/api/risk_score/engine/saved_object/configure:
|
||||
patch:
|
||||
description: Configuring the Risk Engine Saved Object
|
||||
operationId: ConfigureRiskEngineSavedObject
|
||||
requestBody:
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
exclude_alert_statuses:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
exclude_alert_tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
range:
|
||||
type: object
|
||||
properties:
|
||||
end:
|
||||
type: string
|
||||
start:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
risk_engine_saved_object_configured:
|
||||
type: boolean
|
||||
description: Successful response
|
||||
'400':
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_TaskManagerUnavailableResponse'
|
||||
description: Task manager is unavailable
|
||||
default:
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_ConfigureRiskEngineSavedObjectErrorResponse'
|
||||
description: Unexpected error
|
||||
summary: Configure the Risk Engine Saved Object
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
x-beta: true
|
||||
/api/risk_score/engine/schedule_now:
|
||||
post:
|
||||
description: Schedule the risk scoring engine to run as soon as possible. You can use this to recalculate entity risk scores after updating their asset criticality.
|
||||
|
@ -46987,6 +47039,27 @@ components:
|
|||
required:
|
||||
- cleanup_successful
|
||||
- errors
|
||||
Security_Entity_Analytics_API_ConfigureRiskEngineSavedObjectErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
seq:
|
||||
type: integer
|
||||
required:
|
||||
- seq
|
||||
- error
|
||||
type: array
|
||||
risk_engine_saved_object_configured:
|
||||
example: false
|
||||
type: boolean
|
||||
required:
|
||||
- risk_engine_saved_object_configured
|
||||
- errors
|
||||
Security_Entity_Analytics_API_CreateAssetCriticalityRecord:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityRecordIdParts'
|
||||
|
|
|
@ -36270,6 +36270,57 @@ paths:
|
|||
summary: Cleanup the Risk Engine
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/risk_score/engine/saved_object/configure:
|
||||
patch:
|
||||
description: Configuring the Risk Engine Saved Object
|
||||
operationId: ConfigureRiskEngineSavedObject
|
||||
requestBody:
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
exclude_alert_statuses:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
exclude_alert_tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
range:
|
||||
type: object
|
||||
properties:
|
||||
end:
|
||||
type: string
|
||||
start:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
risk_engine_saved_object_configured:
|
||||
type: boolean
|
||||
description: Successful response
|
||||
'400':
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_TaskManagerUnavailableResponse'
|
||||
description: Task manager is unavailable
|
||||
default:
|
||||
content:
|
||||
application/json; Elastic-Api-Version=2023-10-31:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Security_Entity_Analytics_API_ConfigureRiskEngineSavedObjectErrorResponse'
|
||||
description: Unexpected error
|
||||
summary: Configure the Risk Engine Saved Object
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/risk_score/engine/schedule_now:
|
||||
post:
|
||||
description: Schedule the risk scoring engine to run as soon as possible. You can use this to recalculate entity risk scores after updating their asset criticality.
|
||||
|
@ -54672,6 +54723,27 @@ components:
|
|||
required:
|
||||
- cleanup_successful
|
||||
- errors
|
||||
Security_Entity_Analytics_API_ConfigureRiskEngineSavedObjectErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
seq:
|
||||
type: integer
|
||||
required:
|
||||
- seq
|
||||
- error
|
||||
type: array
|
||||
risk_engine_saved_object_configured:
|
||||
example: false
|
||||
type: boolean
|
||||
required:
|
||||
- risk_engine_saved_object_configured
|
||||
- errors
|
||||
Security_Entity_Analytics_API_CreateAssetCriticalityRecord:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityRecordIdParts'
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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: Risk Scoring API
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { z } from '@kbn/zod';
|
||||
|
||||
export type ConfigureRiskEngineSavedObjectErrorResponse = z.infer<
|
||||
typeof ConfigureRiskEngineSavedObjectErrorResponse
|
||||
>;
|
||||
export const ConfigureRiskEngineSavedObjectErrorResponse = z.object({
|
||||
risk_engine_saved_object_configured: z.boolean(),
|
||||
errors: z.array(
|
||||
z.object({
|
||||
seq: z.number().int(),
|
||||
error: z.string(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export type ConfigureRiskEngineSavedObjectRequestBody = z.infer<
|
||||
typeof ConfigureRiskEngineSavedObjectRequestBody
|
||||
>;
|
||||
export const ConfigureRiskEngineSavedObjectRequestBody = z.object({
|
||||
exclude_alert_statuses: z.array(z.string()).optional(),
|
||||
range: z
|
||||
.object({
|
||||
start: z.string().optional(),
|
||||
end: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
exclude_alert_tags: z.array(z.string()).optional(),
|
||||
});
|
||||
export type ConfigureRiskEngineSavedObjectRequestBodyInput = z.input<
|
||||
typeof ConfigureRiskEngineSavedObjectRequestBody
|
||||
>;
|
||||
|
||||
export type ConfigureRiskEngineSavedObjectResponse = z.infer<
|
||||
typeof ConfigureRiskEngineSavedObjectResponse
|
||||
>;
|
||||
export const ConfigureRiskEngineSavedObjectResponse = z.object({
|
||||
risk_engine_saved_object_configured: z.boolean().optional(),
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
version: '2023-10-31'
|
||||
title: Risk Scoring API
|
||||
description: These APIs allow the consumer to configure the Risk Engine Saved Object.
|
||||
paths:
|
||||
/api/risk_score/engine/saved_object/configure:
|
||||
patch:
|
||||
x-labels: [ess, serverless]
|
||||
x-codegen-enabled: true
|
||||
operationId: ConfigureRiskEngineSavedObject
|
||||
summary: Configure the Risk Engine Saved Object
|
||||
description: Configuring the Risk Engine Saved Object
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
exclude_alert_statuses:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
range:
|
||||
type: object
|
||||
properties:
|
||||
start:
|
||||
type: string
|
||||
end:
|
||||
type: string
|
||||
exclude_alert_tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
risk_engine_saved_object_configured:
|
||||
type: boolean
|
||||
'400':
|
||||
description: Task manager is unavailable
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../common/common.schema.yaml#/components/schemas/TaskManagerUnavailableResponse'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ConfigureRiskEngineSavedObjectErrorResponse'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
ConfigureRiskEngineSavedObjectErrorResponse:
|
||||
type: object
|
||||
required:
|
||||
- risk_engine_saved_object_configured
|
||||
- errors
|
||||
properties:
|
||||
risk_engine_saved_object_configured:
|
||||
type: boolean
|
||||
example: false
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- seq
|
||||
- error
|
||||
properties:
|
||||
seq:
|
||||
type: integer
|
||||
error:
|
||||
type: string
|
|
@ -16,3 +16,4 @@ export * from './preview_route.gen';
|
|||
export * from './entity_calculation_route.gen';
|
||||
export * from './get_risk_engine_privileges.gen';
|
||||
export * from './engine_cleanup_route.gen';
|
||||
export * from './engine_configure_saved_object_route.gen';
|
||||
|
|
|
@ -58,9 +58,11 @@ export const RiskScoresPreviewRequest = z.object({
|
|||
/**
|
||||
* A list of alert statuses to exclude from the risk score calculation. If unspecified, all alert statuses are included.
|
||||
*/
|
||||
excludeAlertStatuses: z
|
||||
.array(z.enum(['open', 'closed', 'in-progress', 'acknowledged']))
|
||||
.optional(),
|
||||
exclude_alert_statuses: z.array(z.string()).optional(),
|
||||
/**
|
||||
* A list of alert tags to exclude from the risk score calculation. If unspecified, all alert tags are included.
|
||||
*/
|
||||
exclude_alert_tags: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type RiskScoresPreviewResponse = z.infer<typeof RiskScoresPreviewResponse>;
|
||||
|
|
|
@ -58,16 +58,16 @@ components:
|
|||
description: Defines the time period over which scores will be evaluated. If unspecified, a range of `[now, now-30d]` will be used.
|
||||
weights:
|
||||
$ref: '../common/common.schema.yaml#/components/schemas/RiskScoreWeights'
|
||||
excludeAlertStatuses:
|
||||
exclude_alert_statuses:
|
||||
description: A list of alert statuses to exclude from the risk score calculation. If unspecified, all alert statuses are included.
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- open
|
||||
- closed
|
||||
- in-progress
|
||||
- acknowledged
|
||||
exclude_alert_tags:
|
||||
description: A list of alert tags to exclude from the risk score calculation. If unspecified, all alert tags are included.
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
|
||||
RiskScoresPreviewResponse:
|
||||
|
|
|
@ -276,6 +276,10 @@ import type {
|
|||
GetEntityStoreStatusResponse,
|
||||
} from './entity_analytics/entity_store/status.gen';
|
||||
import type { CleanUpRiskEngineResponse } from './entity_analytics/risk_engine/engine_cleanup_route.gen';
|
||||
import type {
|
||||
ConfigureRiskEngineSavedObjectRequestBodyInput,
|
||||
ConfigureRiskEngineSavedObjectResponse,
|
||||
} from './entity_analytics/risk_engine/engine_configure_saved_object_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';
|
||||
import type { InitRiskEngineResponse } from './entity_analytics/risk_engine/engine_init_route.gen';
|
||||
|
@ -602,6 +606,22 @@ If asset criticality records already exist for the specified entities, those rec
|
|||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
/**
|
||||
* Configuring the Risk Engine Saved Object
|
||||
*/
|
||||
async configureRiskEngineSavedObject(props: ConfigureRiskEngineSavedObjectProps) {
|
||||
this.log.info(`${new Date().toISOString()} Calling API ConfigureRiskEngineSavedObject`);
|
||||
return this.kbnClient
|
||||
.request<ConfigureRiskEngineSavedObjectResponse>({
|
||||
path: '/api/risk_score/engine/saved_object/configure',
|
||||
headers: {
|
||||
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
|
||||
},
|
||||
method: 'PATCH',
|
||||
body: props.body,
|
||||
})
|
||||
.catch(catchAxiosErrorFormatAndThrow);
|
||||
}
|
||||
/**
|
||||
* Copies and returns a timeline or timeline template.
|
||||
|
||||
|
@ -2295,6 +2315,9 @@ export interface BulkUpsertAssetCriticalityRecordsProps {
|
|||
export interface CleanDraftTimelinesProps {
|
||||
body: CleanDraftTimelinesRequestBodyInput;
|
||||
}
|
||||
export interface ConfigureRiskEngineSavedObjectProps {
|
||||
body: ConfigureRiskEngineSavedObjectRequestBodyInput;
|
||||
}
|
||||
export interface CopyTimelineProps {
|
||||
body: CopyTimelineRequestBodyInput;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ export const RISK_ENGINE_SETTINGS_URL = `${RISK_ENGINE_URL}/settings` as const;
|
|||
export const PUBLIC_RISK_ENGINE_URL = `${PUBLIC_RISK_SCORE_URL}/engine` as const;
|
||||
export const RISK_ENGINE_SCHEDULE_NOW_URL = `${RISK_ENGINE_URL}/schedule_now` as const;
|
||||
export const RISK_ENGINE_CLEANUP_URL = `${PUBLIC_RISK_ENGINE_URL}/dangerously_delete_data` as const;
|
||||
export const RISK_ENGINE_CONFIGURE_SO_URL =
|
||||
`${PUBLIC_RISK_ENGINE_URL}/saved_object/configure` as const;
|
||||
|
||||
type ClusterPrivilege = 'manage_index_templates' | 'manage_transform';
|
||||
export const RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES = [
|
||||
|
|
|
@ -646,6 +646,58 @@ paths:
|
|||
summary: Cleanup the Risk Engine
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/risk_score/engine/saved_object/configure:
|
||||
patch:
|
||||
description: Configuring the Risk Engine Saved Object
|
||||
operationId: ConfigureRiskEngineSavedObject
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
exclude_alert_statuses:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
exclude_alert_tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
range:
|
||||
type: object
|
||||
properties:
|
||||
end:
|
||||
type: string
|
||||
start:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
risk_engine_saved_object_configured:
|
||||
type: boolean
|
||||
description: Successful response
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TaskManagerUnavailableResponse'
|
||||
description: Task manager is unavailable
|
||||
default:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: >-
|
||||
#/components/schemas/ConfigureRiskEngineSavedObjectErrorResponse
|
||||
description: Unexpected error
|
||||
summary: Configure the Risk Engine Saved Object
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/risk_score/engine/schedule_now:
|
||||
post:
|
||||
description: >-
|
||||
|
@ -798,6 +850,27 @@ components:
|
|||
required:
|
||||
- cleanup_successful
|
||||
- errors
|
||||
ConfigureRiskEngineSavedObjectErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
seq:
|
||||
type: integer
|
||||
required:
|
||||
- seq
|
||||
- error
|
||||
type: array
|
||||
risk_engine_saved_object_configured:
|
||||
example: false
|
||||
type: boolean
|
||||
required:
|
||||
- risk_engine_saved_object_configured
|
||||
- errors
|
||||
CreateAssetCriticalityRecord:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AssetCriticalityRecordIdParts'
|
||||
|
|
|
@ -646,6 +646,58 @@ paths:
|
|||
summary: Cleanup the Risk Engine
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/risk_score/engine/saved_object/configure:
|
||||
patch:
|
||||
description: Configuring the Risk Engine Saved Object
|
||||
operationId: ConfigureRiskEngineSavedObject
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
exclude_alert_statuses:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
exclude_alert_tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
range:
|
||||
type: object
|
||||
properties:
|
||||
end:
|
||||
type: string
|
||||
start:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
risk_engine_saved_object_configured:
|
||||
type: boolean
|
||||
description: Successful response
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TaskManagerUnavailableResponse'
|
||||
description: Task manager is unavailable
|
||||
default:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: >-
|
||||
#/components/schemas/ConfigureRiskEngineSavedObjectErrorResponse
|
||||
description: Unexpected error
|
||||
summary: Configure the Risk Engine Saved Object
|
||||
tags:
|
||||
- Security Entity Analytics API
|
||||
/api/risk_score/engine/schedule_now:
|
||||
post:
|
||||
description: >-
|
||||
|
@ -798,6 +850,27 @@ components:
|
|||
required:
|
||||
- cleanup_successful
|
||||
- errors
|
||||
ConfigureRiskEngineSavedObjectErrorResponse:
|
||||
type: object
|
||||
properties:
|
||||
errors:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
seq:
|
||||
type: integer
|
||||
required:
|
||||
- seq
|
||||
- error
|
||||
type: array
|
||||
risk_engine_saved_object_configured:
|
||||
example: false
|
||||
type: boolean
|
||||
required:
|
||||
- risk_engine_saved_object_configured
|
||||
- errors
|
||||
CreateAssetCriticalityRecord:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AssetCriticalityRecordIdParts'
|
||||
|
|
|
@ -45,6 +45,7 @@ import {
|
|||
API_VERSIONS,
|
||||
RISK_ENGINE_CLEANUP_URL,
|
||||
RISK_ENGINE_SCHEDULE_NOW_URL,
|
||||
RISK_ENGINE_CONFIGURE_SO_URL,
|
||||
} from '../../../common/constants';
|
||||
import type { SnakeToCamelCase } from '../common/utils';
|
||||
import { useKibana } from '../../common/lib/kibana/kibana_react';
|
||||
|
@ -298,6 +299,14 @@ export const useEntityAnalyticsRoutes = () => {
|
|||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const updateSavedObjectConfiguration = (params: {}) => {
|
||||
http.fetch(RISK_ENGINE_CONFIGURE_SO_URL, {
|
||||
version: API_VERSIONS.public.v1,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
fetchRiskScorePreview,
|
||||
fetchRiskEngineStatus,
|
||||
|
@ -317,6 +326,7 @@ export const useEntityAnalyticsRoutes = () => {
|
|||
calculateEntityRiskScore,
|
||||
cleanUpRiskEngine,
|
||||
fetchEntitiesList,
|
||||
updateSavedObjectConfiguration,
|
||||
};
|
||||
}, [http]);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { TaskManagerUnavailableResponse } from '../../../../common/api/entity_analytics/common';
|
||||
import { useEntityAnalyticsRoutes } from '../api';
|
||||
import type { ConfigureRiskEngineSavedObjectResponse } from '../../../../common/api/entity_analytics/risk_engine/engine_configure_saved_object_route.gen';
|
||||
|
||||
interface ConfigureRiskEngineParams {
|
||||
includeClosedAlerts: boolean;
|
||||
range: { start: string; end: string };
|
||||
}
|
||||
|
||||
export const useConfigureSORiskEngineMutation = (
|
||||
options?: UseMutationOptions<
|
||||
ConfigureRiskEngineSavedObjectResponse,
|
||||
{ body: ConfigureRiskEngineSavedObjectResponse | TaskManagerUnavailableResponse },
|
||||
ConfigureRiskEngineParams
|
||||
>
|
||||
) => {
|
||||
const { updateSavedObjectConfiguration } = useEntityAnalyticsRoutes();
|
||||
|
||||
return useMutation<
|
||||
ConfigureRiskEngineSavedObjectResponse,
|
||||
{ body: ConfigureRiskEngineSavedObjectResponse | TaskManagerUnavailableResponse },
|
||||
ConfigureRiskEngineParams
|
||||
>(async (params: ConfigureRiskEngineParams) => {
|
||||
await updateSavedObjectConfiguration({
|
||||
exclude_alert_statuses: params.includeClosedAlerts ? [] : ['closed'],
|
||||
range: params.range,
|
||||
});
|
||||
return { risk_engine_saved_object_configured: true };
|
||||
}, options);
|
||||
};
|
|
@ -17,11 +17,12 @@ export const useRiskScorePreview = ({
|
|||
data_view_id: dataViewId,
|
||||
range,
|
||||
filter,
|
||||
exclude_alert_statuses: excludeAlertStatuses,
|
||||
}: UseRiskScorePreviewParams) => {
|
||||
const { fetchRiskScorePreview } = useEntityAnalyticsRoutes();
|
||||
|
||||
return useQuery(
|
||||
['POST', 'FETCH_PREVIEW_RISK_SCORE', range, filter],
|
||||
['POST', 'FETCH_PREVIEW_RISK_SCORE', range, filter, excludeAlertStatuses],
|
||||
async ({ signal }) => {
|
||||
if (!dataViewId) {
|
||||
return;
|
||||
|
@ -49,6 +50,10 @@ export const useRiskScorePreview = ({
|
|||
params.filter = filter;
|
||||
}
|
||||
|
||||
if (excludeAlertStatuses) {
|
||||
params.exclude_alert_statuses = excludeAlertStatuses;
|
||||
}
|
||||
|
||||
const response = await fetchRiskScorePreview({ signal, params });
|
||||
|
||||
return response;
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RiskScoreConfigurationSection renders correctly 1`] = `
|
||||
<Fragment>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
>
|
||||
<div>
|
||||
<EuiSwitch
|
||||
checked={false}
|
||||
data-test-subj="includeClosedAlertsSwitch"
|
||||
label="Include closed alerts for risk scoring"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</div>
|
||||
<Styled(div) />
|
||||
<div>
|
||||
<EuiSuperDatePicker
|
||||
compressed={false}
|
||||
end="now"
|
||||
onTimeChange={[Function]}
|
||||
showUpdateButton={false}
|
||||
start="now-30m"
|
||||
width="auto"
|
||||
/>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiText
|
||||
size="s"
|
||||
>
|
||||
<p>
|
||||
Enable this option to factor both open and closed alerts into the risk engine
|
||||
calculations. Including closed alerts helps provide a more comprehensive risk assessment
|
||||
based on past incidents, leading to more accurate scoring and insights.
|
||||
</p>
|
||||
</EuiText>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 { RiskScoreConfigurationSection } from './risk_score_configuration_section';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EuiSuperDatePicker, EuiSwitch } from '@elastic/eui';
|
||||
import * as i18n from '../translations';
|
||||
import { useAppToasts } from '../../common/hooks/use_app_toasts';
|
||||
import { useConfigureSORiskEngineMutation } from '../api/hooks/use_configure_risk_engine_saved_object';
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
jest.mock('../../common/hooks/use_app_toasts');
|
||||
jest.mock('../api/hooks/use_configure_risk_engine_saved_object');
|
||||
|
||||
describe('RiskScoreConfigurationSection', () => {
|
||||
const mockConfigureSO = useConfigureSORiskEngineMutation as jest.Mock;
|
||||
const defaultProps = {
|
||||
includeClosedAlerts: false,
|
||||
setIncludeClosedAlerts: jest.fn(),
|
||||
from: 'now-30m',
|
||||
to: 'now',
|
||||
onDateChange: jest.fn(),
|
||||
};
|
||||
|
||||
const mockAddSuccess = jest.fn();
|
||||
const mockMutate = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
(useAppToasts as jest.Mock).mockReturnValue({ addSuccess: mockAddSuccess });
|
||||
mockConfigureSO.mockReturnValue({ mutate: mockMutate });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
const wrapper = shallow(<RiskScoreConfigurationSection {...defaultProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('toggles includeClosedAlerts', () => {
|
||||
const wrapper = mount(
|
||||
<RiskScoreConfigurationSection {...defaultProps} includeClosedAlerts={true} />
|
||||
);
|
||||
wrapper.find(EuiSwitch).simulate('click');
|
||||
expect(defaultProps.setIncludeClosedAlerts).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('calls onDateChange on date change', () => {
|
||||
const wrapper = mount(<RiskScoreConfigurationSection {...defaultProps} />);
|
||||
wrapper.find(EuiSuperDatePicker).props().onTimeChange({ start: 'now-30m', end: 'now' });
|
||||
expect(defaultProps.onDateChange).toHaveBeenCalledWith({ start: 'now-30m', end: 'now' });
|
||||
});
|
||||
|
||||
it('shows bottom bar when changes are made', async () => {
|
||||
const wrapper = mount(
|
||||
<RiskScoreConfigurationSection {...defaultProps} includeClosedAlerts={false} />
|
||||
);
|
||||
wrapper.find(EuiSwitch).simulate('click');
|
||||
wrapper.find(EuiSuperDatePicker).props().onTimeChange({ start: 'now-14m', end: 'now' });
|
||||
wrapper.update();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0)); // wait for the component to update
|
||||
expect(wrapper.find('EuiBottomBar').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('saves changes', () => {
|
||||
const wrapper = mount(
|
||||
<RiskScoreConfigurationSection {...defaultProps} includeClosedAlerts={true} />
|
||||
);
|
||||
|
||||
// Simulate clicking the switch
|
||||
const closedAlertsToggle = wrapper.find('button[data-test-subj="includeClosedAlertsSwitch"]');
|
||||
expect(closedAlertsToggle.exists()).toBe(true);
|
||||
closedAlertsToggle.simulate('click');
|
||||
|
||||
wrapper.update();
|
||||
|
||||
const saveChangesButton = wrapper.find('button[data-test-subj="riskScoreSaveButton"]');
|
||||
expect(saveChangesButton.exists()).toBe(true);
|
||||
saveChangesButton.simulate('click');
|
||||
const callArgs = mockMutate.mock.calls[0][0];
|
||||
expect(callArgs).toEqual({
|
||||
includeClosedAlerts: true,
|
||||
range: { start: 'now-30m', end: 'now' },
|
||||
});
|
||||
});
|
||||
|
||||
it('shows success toast on save', () => {
|
||||
const wrapper = mount(
|
||||
<RiskScoreConfigurationSection {...defaultProps} includeClosedAlerts={true} />
|
||||
);
|
||||
|
||||
act(() => {
|
||||
wrapper.find('button[data-test-subj="includeClosedAlertsSwitch"]').simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
act(() => {
|
||||
wrapper.find('button[data-test-subj="riskScoreSaveButton"]').simulate('click');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
mockMutate.mock.calls[0][1].onSuccess();
|
||||
});
|
||||
|
||||
expect(mockAddSuccess).toHaveBeenCalledWith(
|
||||
i18n.RISK_ENGINE_SAVED_OBJECT_CONFIGURATION_SUCCESS,
|
||||
{
|
||||
toastLifeTimeMs: 5000,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
EuiSuperDatePicker,
|
||||
EuiButton,
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiSwitch,
|
||||
EuiFlexItem,
|
||||
EuiBottomBar,
|
||||
EuiButtonEmpty,
|
||||
EuiSpacer,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import { useAppToasts } from '../../common/hooks/use_app_toasts';
|
||||
import * as i18n from '../translations';
|
||||
import { useConfigureSORiskEngineMutation } from '../api/hooks/use_configure_risk_engine_saved_object';
|
||||
import { getEntityAnalyticsRiskScorePageStyles } from './risk_score_page_styles';
|
||||
|
||||
export const RiskScoreConfigurationSection = ({
|
||||
includeClosedAlerts,
|
||||
setIncludeClosedAlerts,
|
||||
from,
|
||||
to,
|
||||
onDateChange,
|
||||
}: {
|
||||
includeClosedAlerts: boolean;
|
||||
setIncludeClosedAlerts: (value: boolean) => void;
|
||||
from: string;
|
||||
to: string;
|
||||
onDateChange: ({ start, end }: { start: string; end: string }) => void;
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const styles = getEntityAnalyticsRiskScorePageStyles(euiTheme);
|
||||
const [start, setFrom] = useState(from);
|
||||
const [end, setTo] = useState(to);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showBar, setShowBar] = useState(false);
|
||||
const { addSuccess } = useAppToasts();
|
||||
const initialIncludeClosedAlerts = useRef(includeClosedAlerts);
|
||||
const initialStart = useRef(from);
|
||||
const initialEnd = useRef(to);
|
||||
|
||||
const [savedIncludeClosedAlerts, setSavedIncludeClosedAlerts] = useLocalStorage(
|
||||
'includeClosedAlerts',
|
||||
includeClosedAlerts ?? false
|
||||
);
|
||||
const [savedStart, setSavedStart] = useLocalStorage(
|
||||
'entityAnalytics:riskScoreConfiguration:fromDate',
|
||||
from
|
||||
);
|
||||
const [savedEnd, setSavedEnd] = useLocalStorage(
|
||||
'entityAnalytics:riskScoreConfiguration:toDate',
|
||||
to
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (savedIncludeClosedAlerts !== null && savedIncludeClosedAlerts !== undefined) {
|
||||
initialIncludeClosedAlerts.current = savedIncludeClosedAlerts;
|
||||
setIncludeClosedAlerts(savedIncludeClosedAlerts);
|
||||
}
|
||||
if (savedStart && savedEnd) {
|
||||
initialStart.current = savedStart;
|
||||
initialEnd.current = savedEnd;
|
||||
setFrom(savedStart);
|
||||
setTo(savedEnd);
|
||||
}
|
||||
}, [savedIncludeClosedAlerts, savedStart, savedEnd, setIncludeClosedAlerts]);
|
||||
|
||||
const onRefresh = ({ start: newStart, end: newEnd }: { start: string; end: string }) => {
|
||||
setFrom(newStart);
|
||||
setTo(newEnd);
|
||||
onDateChange({ start: newStart, end: newEnd });
|
||||
checkForChanges(newStart, newEnd, includeClosedAlerts);
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
const newValue = !includeClosedAlerts;
|
||||
setIncludeClosedAlerts(newValue);
|
||||
checkForChanges(start, end, newValue);
|
||||
};
|
||||
|
||||
const checkForChanges = (newStart: string, newEnd: string, newIncludeClosedAlerts: boolean) => {
|
||||
if (
|
||||
newStart !== initialStart.current ||
|
||||
newEnd !== initialEnd.current ||
|
||||
newIncludeClosedAlerts !== initialIncludeClosedAlerts.current
|
||||
) {
|
||||
setShowBar(true);
|
||||
} else {
|
||||
setShowBar(false);
|
||||
}
|
||||
};
|
||||
|
||||
const { mutate } = useConfigureSORiskEngineMutation();
|
||||
|
||||
const handleSave = () => {
|
||||
setIsLoading(true);
|
||||
mutate(
|
||||
{
|
||||
includeClosedAlerts,
|
||||
range: { start, end },
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setShowBar(false);
|
||||
addSuccess(i18n.RISK_ENGINE_SAVED_OBJECT_CONFIGURATION_SUCCESS, {
|
||||
toastLifeTimeMs: 5000,
|
||||
});
|
||||
setIsLoading(false);
|
||||
|
||||
initialStart.current = start;
|
||||
initialEnd.current = end;
|
||||
initialIncludeClosedAlerts.current = includeClosedAlerts;
|
||||
|
||||
setSavedIncludeClosedAlerts(includeClosedAlerts);
|
||||
setSavedStart(start);
|
||||
setSavedEnd(end);
|
||||
},
|
||||
onError: () => {
|
||||
setIsLoading(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<div>
|
||||
<EuiSwitch
|
||||
label={i18n.INCLUDE_CLOSED_ALERTS_LABEL}
|
||||
checked={includeClosedAlerts}
|
||||
onChange={handleToggle}
|
||||
data-test-subj="includeClosedAlertsSwitch"
|
||||
/>
|
||||
</div>
|
||||
<styles.VerticalSeparator />
|
||||
<div>
|
||||
<EuiSuperDatePicker
|
||||
start={start}
|
||||
end={end}
|
||||
onTimeChange={onRefresh}
|
||||
width={'auto'}
|
||||
compressed={false}
|
||||
showUpdateButton={false}
|
||||
/>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="s">
|
||||
<p>{i18n.RISK_ENGINE_INCLUDE_CLOSED_ALERTS_DESCRIPTION}</p>
|
||||
</EuiText>
|
||||
{showBar && (
|
||||
<EuiBottomBar paddingSize="s" position="fixed">
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
color="text"
|
||||
size="s"
|
||||
iconType="cross"
|
||||
onClick={() => {
|
||||
setShowBar(false);
|
||||
setFrom(initialStart.current);
|
||||
setTo(initialEnd.current);
|
||||
setIncludeClosedAlerts(initialIncludeClosedAlerts.current);
|
||||
}}
|
||||
>
|
||||
{i18n.DISCARD_CHANGES}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
size="s"
|
||||
iconType="check"
|
||||
onClick={handleSave}
|
||||
isLoading={isLoading}
|
||||
data-test-subj="riskScoreSaveButton"
|
||||
>
|
||||
{i18n.SAVE_CHANGES}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiBottomBar>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -10,11 +10,8 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHealth,
|
||||
EuiHorizontalRule,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiTitle,
|
||||
EuiLoadingSpinner,
|
||||
EuiBadge,
|
||||
EuiButtonEmpty,
|
||||
|
@ -28,8 +25,6 @@ import {
|
|||
EuiCallOut,
|
||||
EuiAccordion,
|
||||
} from '@elastic/eui';
|
||||
import { LinkAnchor } from '@kbn/security-solution-navigation/links';
|
||||
import { SecurityPageName } from '@kbn/security-solution-navigation';
|
||||
import type { RiskEngineStatus } from '../../../common/api/entity_analytics/risk_engine/engine_status_route.gen';
|
||||
import { RiskEngineStatusEnum } from '../../../common/api/entity_analytics/risk_engine/engine_status_route.gen';
|
||||
import * as i18n from '../translations';
|
||||
|
@ -38,8 +33,6 @@ import { useInitRiskEngineMutation } from '../api/hooks/use_init_risk_engine_mut
|
|||
import { useEnableRiskEngineMutation } from '../api/hooks/use_enable_risk_engine_mutation';
|
||||
import { useDisableRiskEngineMutation } from '../api/hooks/use_disable_risk_engine_mutation';
|
||||
import { useAppToasts } from '../../common/hooks/use_app_toasts';
|
||||
import { RiskInformationFlyout } from './risk_information';
|
||||
import { useOnOpenCloseHandler } from '../../helper_hooks';
|
||||
import type { RiskEngineMissingPrivilegesResponse } from '../hooks/use_missing_risk_engine_privileges';
|
||||
|
||||
const MIN_WIDTH_TO_PREVENT_LABEL_FROM_MOVING = '50px';
|
||||
|
@ -144,12 +137,12 @@ const RiskEngineHealth: React.FC<{ currentRiskEngineStatus?: RiskEngineStatus |
|
|||
currentRiskEngineStatus,
|
||||
}) => {
|
||||
if (!currentRiskEngineStatus) {
|
||||
return <EuiHealth color="subdued">{'-'}</EuiHealth>;
|
||||
return <EuiHealth color="danger">{'-'}</EuiHealth>;
|
||||
}
|
||||
if (currentRiskEngineStatus === RiskEngineStatusEnum.ENABLED) {
|
||||
return <EuiHealth color="success">{i18n.RISK_SCORE_MODULE_STATUS_ON}</EuiHealth>;
|
||||
}
|
||||
return <EuiHealth color="subdued">{i18n.RISK_SCORE_MODULE_STATUS_OFF}</EuiHealth>;
|
||||
return <EuiHealth color="danger">{i18n.RISK_SCORE_MODULE_STATUS_OFF}</EuiHealth>;
|
||||
};
|
||||
|
||||
const RiskEngineStatusRow: React.FC<{
|
||||
|
@ -181,7 +174,6 @@ const RiskEngineStatusRow: React.FC<{
|
|||
data-test-subj="risk-score-switch"
|
||||
checked={currentRiskEngineStatus === RiskEngineStatusEnum.ENABLED}
|
||||
onChange={onSwitchClick}
|
||||
compressed
|
||||
disabled={btnIsDisabled}
|
||||
aria-describedby={'switchRiskModule'}
|
||||
/>
|
||||
|
@ -221,8 +213,6 @@ export const RiskScoreEnableSection: React.FC<{
|
|||
const closeModal = () => setIsModalVisible(false);
|
||||
const showModal = () => setIsModalVisible(true);
|
||||
|
||||
const [isFlyoutVisible, handleOnOpen, handleOnClose] = useOnOpenCloseHandler();
|
||||
|
||||
const isLoading =
|
||||
initRiskEngineMutation.isLoading ||
|
||||
enableRiskEngineMutation.isLoading ||
|
||||
|
@ -254,9 +244,6 @@ export const RiskScoreEnableSection: React.FC<{
|
|||
return (
|
||||
<>
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>{i18n.RISK_SCORE_MODULE_STATUS}</h2>
|
||||
</EuiTitle>
|
||||
{initRiskEngineMutation.isError && <RiskScoreErrorPanel errors={initRiskEngineErrors} />}
|
||||
{disableRiskEngineMutation.isError && (
|
||||
<RiskScoreErrorPanel errors={[disableRiskEngineMutation.error.body.message]} />
|
||||
|
@ -273,12 +260,10 @@ export const RiskScoreEnableSection: React.FC<{
|
|||
isLoading={initRiskEngineMutation.isLoading}
|
||||
closeModal={closeModal}
|
||||
/>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems={'baseline'}>
|
||||
{i18n.ENTITY_RISK_SCORING}
|
||||
{isUpdateAvailable && <EuiBadge color="success">{i18n.UPDATE_AVAILABLE}</EuiBadge>}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
@ -310,29 +295,9 @@ export const RiskScoreEnableSection: React.FC<{
|
|||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
<EuiSpacer />
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>{i18n.USEFUL_LINKS}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<ul>
|
||||
<li>
|
||||
<LinkAnchor id={SecurityPageName.entityAnalytics}>{i18n.EA_DASHBOARD_LINK}</LinkAnchor>
|
||||
<EuiSpacer size="s" />
|
||||
</li>
|
||||
<li>
|
||||
<EuiLink onClick={handleOnOpen} data-test-subj="open-risk-information-flyout-trigger">
|
||||
{i18n.EA_DOCS_ENTITY_RISK_SCORE}
|
||||
</EuiLink>
|
||||
{isFlyoutVisible && <RiskInformationFlyout handleOnClose={handleOnClose} />}
|
||||
<EuiSpacer size="s" />
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 styled from '@emotion/styled';
|
||||
import type { EuiThemeComputed } from '@elastic/eui';
|
||||
interface EntityAnalyticsRiskScorePageStyles {
|
||||
VerticalSeparator: ReturnType<typeof styled.div>;
|
||||
}
|
||||
|
||||
export const getEntityAnalyticsRiskScorePageStyles = (
|
||||
euiTheme: EuiThemeComputed
|
||||
): EntityAnalyticsRiskScorePageStyles => ({
|
||||
VerticalSeparator: styled.div`
|
||||
:before {
|
||||
content: '';
|
||||
height: ${euiTheme.size.l};
|
||||
border-left: ${euiTheme.border.width.thin} solid ${euiTheme.colors.lightShade};
|
||||
}
|
||||
`,
|
||||
});
|
|
@ -5,11 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
|
@ -22,8 +20,7 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiCode,
|
||||
} from '@elastic/eui';
|
||||
import type { BoolQuery, TimeRange, Query } from '@kbn/es-query';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import type { BoolQuery } from '@kbn/es-query';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { EntityRiskScoreRecord } from '../../../common/api/entity_analytics/common';
|
||||
import {
|
||||
|
@ -33,10 +30,8 @@ import {
|
|||
import { RiskScorePreviewTable } from './risk_score_preview_table';
|
||||
import * as i18n from '../translations';
|
||||
import { useRiskScorePreview } from '../api/hooks/use_preview_risk_scores';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { SourcererScopeName } from '../../sourcerer/store/model';
|
||||
import { useSourcererDataView } from '../../sourcerer/containers';
|
||||
import { useAppToasts } from '../../common/hooks/use_app_toasts';
|
||||
import type { RiskEngineMissingPrivilegesResponse } from '../hooks/use_missing_risk_engine_privileges';
|
||||
import { userHasRiskEngineReadPermissions } from '../common';
|
||||
interface IRiskScorePreviewPanel {
|
||||
|
@ -55,7 +50,10 @@ const getRiskiestScores = (scores: EntityRiskScoreRecord[] = [], field: string)
|
|||
|
||||
export const RiskScorePreviewSection: React.FC<{
|
||||
privileges: RiskEngineMissingPrivilegesResponse;
|
||||
}> = ({ privileges }) => {
|
||||
includeClosedAlerts: boolean;
|
||||
from: string;
|
||||
to: string;
|
||||
}> = ({ privileges, includeClosedAlerts, from, to }) => {
|
||||
const sectionBody = useMemo(() => {
|
||||
if (privileges.isLoading) {
|
||||
return (
|
||||
|
@ -67,11 +65,11 @@ export const RiskScorePreviewSection: React.FC<{
|
|||
);
|
||||
}
|
||||
if (userHasRiskEngineReadPermissions(privileges)) {
|
||||
return <RiskEnginePreview />;
|
||||
return <RiskEnginePreview includeClosedAlerts={includeClosedAlerts} from={from} to={to} />;
|
||||
}
|
||||
|
||||
return <MissingPermissionsCallout />;
|
||||
}, [privileges]);
|
||||
}, [privileges, includeClosedAlerts, from, to]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -138,65 +136,30 @@ const RiskScorePreviewPanel = ({
|
|||
);
|
||||
};
|
||||
|
||||
const RiskEnginePreview = () => {
|
||||
const [dateRange, setDateRange] = useState<{ from: string; to: string }>({
|
||||
from: 'now-24h',
|
||||
to: 'now',
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<{ bool: BoolQuery }>({
|
||||
const RiskEnginePreview: React.FC<{ includeClosedAlerts: boolean; from: string; to: string }> = ({
|
||||
includeClosedAlerts,
|
||||
from,
|
||||
to,
|
||||
}) => {
|
||||
const [filters] = useState<{ bool: BoolQuery }>({
|
||||
bool: { must: [], filter: [], should: [], must_not: [] },
|
||||
});
|
||||
|
||||
const [dataViewsArray, setDataViewsArray] = useState<DataView[]>([]);
|
||||
|
||||
const {
|
||||
unifiedSearch: {
|
||||
ui: { SearchBar },
|
||||
},
|
||||
dataViews,
|
||||
} = useKibana().services;
|
||||
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
const { sourcererDataView } = useSourcererDataView(SourcererScopeName.detections);
|
||||
|
||||
const { data, isLoading, refetch, isError } = useRiskScorePreview({
|
||||
data_view_id: sourcererDataView.title,
|
||||
filter: filters,
|
||||
range: {
|
||||
start: dateRange.from,
|
||||
end: dateRange.to,
|
||||
start: from,
|
||||
end: to,
|
||||
},
|
||||
exclude_alert_statuses: includeClosedAlerts ? [] : ['closed'],
|
||||
});
|
||||
|
||||
const hosts = getRiskiestScores(data?.scores.host, 'host.name');
|
||||
const users = getRiskiestScores(data?.scores.user, 'user.name');
|
||||
|
||||
const onQuerySubmit = useCallback(
|
||||
(payload: { dateRange: TimeRange; query?: Query }) => {
|
||||
setDateRange({
|
||||
from: payload.dateRange.from,
|
||||
to: payload.dateRange.to,
|
||||
});
|
||||
try {
|
||||
const newFilters = buildEsQuery(
|
||||
undefined,
|
||||
payload.query ?? { query: '', language: 'kuery' },
|
||||
[]
|
||||
);
|
||||
setFilters(newFilters);
|
||||
} catch (e) {
|
||||
addError(e, { title: i18n.PREVIEW_QUERY_ERROR_TITLE });
|
||||
}
|
||||
},
|
||||
[addError, setDateRange, setFilters]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dataViews.create(sourcererDataView).then((dataView) => setDataViewsArray([dataView]));
|
||||
}, [dataViews, sourcererDataView]);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
|
@ -220,23 +183,8 @@ const RiskEnginePreview = () => {
|
|||
return (
|
||||
<>
|
||||
<EuiText>{i18n.PREVIEW_DESCRIPTION}</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow fullWidth data-test-subj="risk-score-preview-search-bar">
|
||||
<SearchBar
|
||||
appName="siem"
|
||||
isLoading={isLoading}
|
||||
indexPatterns={dataViewsArray}
|
||||
dateRangeFrom={dateRange.from}
|
||||
dateRangeTo={dateRange.to}
|
||||
onQuerySubmit={onQuerySubmit}
|
||||
showFilterBar={false}
|
||||
showDatePicker={true}
|
||||
displayStyle={'inPage'}
|
||||
submitButtonStyle={'iconOnly'}
|
||||
dataTestSubj="risk-score-preview-search-bar-input"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer />
|
||||
<EuiSpacer />
|
||||
|
||||
<RiskScorePreviewPanel
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { LinkAnchor } from '@kbn/security-solution-navigation/links';
|
||||
import { SecurityPageName } from '@kbn/security-solution-navigation';
|
||||
import styled from '@emotion/styled';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import * as i18n from '../translations';
|
||||
import { RiskInformationFlyout } from './risk_information';
|
||||
|
||||
const StyledList = styled.ul`
|
||||
list-style-type: disc;
|
||||
padding-left: ${euiThemeVars.euiSizeM};
|
||||
`;
|
||||
|
||||
export const RiskScoreUsefulLinksSection = () => {
|
||||
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
|
||||
|
||||
const handleOnOpen = () => setIsFlyoutVisible(true);
|
||||
const handleOnClose = () => setIsFlyoutVisible(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>{i18n.USEFUL_LINKS}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<StyledList>
|
||||
<li>
|
||||
<LinkAnchor id={SecurityPageName.entityAnalytics}>{i18n.EA_DASHBOARD_LINK}</LinkAnchor>
|
||||
<EuiSpacer size="s" />
|
||||
</li>
|
||||
<li>
|
||||
<EuiLink onClick={handleOnOpen} data-test-subj="open-risk-information-flyout-trigger">
|
||||
{i18n.EA_DOCS_ENTITY_RISK_SCORE}
|
||||
</EuiLink>
|
||||
{isFlyoutVisible && <RiskInformationFlyout handleOnClose={handleOnClose} />}
|
||||
<EuiSpacer size="s" />
|
||||
</li>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -5,38 +5,164 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiPageHeader, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPageHeader,
|
||||
EuiHorizontalRule,
|
||||
EuiButton,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import { RiskScorePreviewSection } from '../components/risk_score_preview_section';
|
||||
import { RiskScoreEnableSection } from '../components/risk_score_enable_section';
|
||||
import { ENTITY_ANALYTICS_RISK_SCORE } from '../../app/translations';
|
||||
import { BETA } from '../../common/translations';
|
||||
import { RiskEnginePrivilegesCallOut } from '../components/risk_engine_privileges_callout';
|
||||
import { useMissingRiskEnginePrivileges } from '../hooks/use_missing_risk_engine_privileges';
|
||||
import { RiskScoreUsefulLinksSection } from '../components/risk_score_useful_links_section';
|
||||
import { RiskScoreConfigurationSection } from '../components/risk_score_configuration_section';
|
||||
import { useRiskEngineStatus } from '../api/hooks/use_risk_engine_status';
|
||||
import { useScheduleNowRiskEngineMutation } from '../api/hooks/use_schedule_now_risk_engine_mutation';
|
||||
import { useAppToasts } from '../../common/hooks/use_app_toasts';
|
||||
import * as i18n from '../translations';
|
||||
import { getEntityAnalyticsRiskScorePageStyles } from '../components/risk_score_page_styles';
|
||||
|
||||
const TEN_SECONDS = 10000;
|
||||
|
||||
export const EntityAnalyticsManagementPage = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const styles = getEntityAnalyticsRiskScorePageStyles(euiTheme);
|
||||
const privileges = useMissingRiskEnginePrivileges();
|
||||
const [includeClosedAlerts, setIncludeClosedAlerts] = useState(false);
|
||||
const [from, setFrom] = useState(localStorage.getItem('dateStart') || 'now-30m');
|
||||
const [to, setTo] = useState(localStorage.getItem('dateEnd') || 'now');
|
||||
const { data: riskEngineStatus } = useRiskEngineStatus({
|
||||
refetchInterval: TEN_SECONDS,
|
||||
structuralSharing: false, // Force the component to rerender after every Risk Engine Status API call
|
||||
});
|
||||
const currentRiskEngineStatus = riskEngineStatus?.risk_engine_status;
|
||||
const runEngineEnabled = currentRiskEngineStatus === 'ENABLED';
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { mutate: scheduleNowRiskEngine } = useScheduleNowRiskEngineMutation();
|
||||
const { addSuccess, addError } = useAppToasts();
|
||||
|
||||
const handleRunEngineClick = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
scheduleNowRiskEngine();
|
||||
|
||||
if (!isLoading) {
|
||||
addSuccess(i18n.RISK_SCORE_ENGINE_RUN_SUCCESS, { toastLifeTimeMs: 5000 });
|
||||
}
|
||||
} catch (error) {
|
||||
addError(error, {
|
||||
title: i18n.RISK_SCORE_ENGINE_RUN_FAILURE,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIncludeClosedAlertsToggle = useCallback(
|
||||
(value: boolean) => {
|
||||
setIncludeClosedAlerts(value);
|
||||
},
|
||||
[setIncludeClosedAlerts]
|
||||
);
|
||||
|
||||
const handleDateChange = ({ start, end }: { start: string; end: string }) => {
|
||||
setFrom(start);
|
||||
setTo(end);
|
||||
localStorage.setItem('dateStart', start);
|
||||
localStorage.setItem('dateEnd', end);
|
||||
};
|
||||
|
||||
const { status, runAt } = riskEngineStatus?.risk_engine_task_status || {};
|
||||
|
||||
const isRunning = status === 'running' || (!!runAt && new Date(runAt) < new Date());
|
||||
|
||||
const formatTimeFromNow = (time: string | undefined): string => {
|
||||
if (!time) {
|
||||
return '';
|
||||
}
|
||||
return i18n.RISK_ENGINE_NEXT_RUN_TIME(moment(time).fromNow(true));
|
||||
};
|
||||
|
||||
const countDownText = isRunning
|
||||
? 'Now running'
|
||||
: formatTimeFromNow(riskEngineStatus?.risk_engine_task_status?.runAt);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RiskEnginePrivilegesCallOut privileges={privileges} />
|
||||
<EuiPageHeader
|
||||
pageTitle={
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
{/* Page Title */}
|
||||
<EuiFlexItem data-test-subj="entityAnalyticsManagementPageTitle" grow={false}>
|
||||
{ENTITY_ANALYTICS_RISK_SCORE}
|
||||
</EuiFlexItem>
|
||||
<EuiBetaBadge label={BETA} size="s" />
|
||||
|
||||
{/* Controls Section */}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="m">
|
||||
{/* Run Engine Section */}
|
||||
{runEngineEnabled && (
|
||||
<>
|
||||
{/* Run Engine Button */}
|
||||
<EuiButton
|
||||
size="s"
|
||||
iconType="play"
|
||||
isLoading={isLoading}
|
||||
onClick={handleRunEngineClick}
|
||||
>
|
||||
{i18n.RUN_RISK_SCORE_ENGINE}
|
||||
</EuiButton>
|
||||
|
||||
{/* Vertical Line */}
|
||||
<styles.VerticalSeparator />
|
||||
|
||||
{/* Countdown Text */}
|
||||
<div>
|
||||
<EuiText size="s" color="subdued">
|
||||
{countDownText}
|
||||
</EuiText>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Risk Score Enable Section */}
|
||||
<div>
|
||||
<RiskScoreEnableSection privileges={privileges} />
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup gutterSize="xl">
|
||||
|
||||
<EuiHorizontalRule />
|
||||
<EuiFlexGroup gutterSize="xl" alignItems="flexStart">
|
||||
<EuiFlexItem grow={2}>
|
||||
<RiskScoreEnableSection privileges={privileges} />
|
||||
<RiskScoreConfigurationSection
|
||||
includeClosedAlerts={includeClosedAlerts}
|
||||
setIncludeClosedAlerts={handleIncludeClosedAlertsToggle}
|
||||
from={from}
|
||||
to={to}
|
||||
onDateChange={handleDateChange}
|
||||
/>
|
||||
<EuiHorizontalRule />
|
||||
<RiskScoreUsefulLinksSection />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<RiskScorePreviewSection privileges={privileges} />
|
||||
<RiskScorePreviewSection
|
||||
privileges={privileges}
|
||||
includeClosedAlerts={includeClosedAlerts}
|
||||
from={from}
|
||||
to={to}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
|
|
|
@ -302,3 +302,64 @@ export const RISK_SCORE_MODULE_TURNED_OFF = i18n.translate(
|
|||
defaultMessage: 'Entity risk score has been turned off',
|
||||
}
|
||||
);
|
||||
|
||||
export const RISK_SCORE_ENGINE_RUN_SUCCESS = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.engineRunSuccess',
|
||||
{
|
||||
defaultMessage: 'Entity risk score engine started successfully',
|
||||
}
|
||||
);
|
||||
|
||||
export const RISK_ENGINE_SAVED_OBJECT_CONFIGURATION_SUCCESS = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.savedObject.configurationSuccess',
|
||||
{
|
||||
defaultMessage: 'Risk engine Saved Object configuration updated successfully',
|
||||
}
|
||||
);
|
||||
|
||||
export const INCLUDE_CLOSED_ALERTS_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.includeClosedAlertsLabel',
|
||||
{
|
||||
defaultMessage: 'Include closed alerts for risk scoring',
|
||||
}
|
||||
);
|
||||
|
||||
export const RISK_ENGINE_INCLUDE_CLOSED_ALERTS_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.includeClosedAlertsDescription',
|
||||
{
|
||||
defaultMessage: `Enable this option to factor both open and closed alerts into the risk engine
|
||||
calculations. Including closed alerts helps provide a more comprehensive risk assessment
|
||||
based on past incidents, leading to more accurate scoring and insights.`,
|
||||
}
|
||||
);
|
||||
|
||||
export const RISK_ENGINE_NEXT_RUN_TIME = (timeInMinutes: string) =>
|
||||
i18n.translate('xpack.securitySolution.riskScore.engineNextRunTime', {
|
||||
defaultMessage: `Next engine run in {timeInMinutes}`,
|
||||
values: { timeInMinutes },
|
||||
});
|
||||
|
||||
export const RUN_RISK_SCORE_ENGINE = i18n.translate('xpack.securitySolution.riskScore.runEngine', {
|
||||
defaultMessage: 'Run Engine',
|
||||
});
|
||||
|
||||
export const SAVE_CHANGES = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.engineSavedObjectsaveChanges',
|
||||
{
|
||||
defaultMessage: 'Save',
|
||||
}
|
||||
);
|
||||
|
||||
export const DISCARD_CHANGES = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.engineSavedObject.discardChanges',
|
||||
{
|
||||
defaultMessage: 'Discard',
|
||||
}
|
||||
);
|
||||
|
||||
export const RISK_SCORE_ENGINE_RUN_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.engineRunSuccess',
|
||||
{
|
||||
defaultMessage: 'Entity risk score engine failed to start',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -17,4 +17,5 @@ export enum RiskEngineAuditActions {
|
|||
RISK_ENGINE_DISABLE_LEGACY_ENGINE = 'risk_engine_disable_legacy_engine',
|
||||
RISK_ENGINE_REMOVE_TASK = 'risk_engine_remove_task',
|
||||
RISK_ENGINE_SCHEDULE_NOW = 'risk_engine_schedule_now',
|
||||
RISK_ENGINE_CONFIGURE_SAVED_OBJECT = 'risk_engine_configure_saved_object',
|
||||
}
|
||||
|
|
|
@ -82,10 +82,13 @@ export class RiskEngineDataClient {
|
|||
}
|
||||
|
||||
try {
|
||||
await initSavedObjects({
|
||||
const soResult = await initSavedObjects({
|
||||
savedObjectsClient: this.options.soClient,
|
||||
namespace,
|
||||
});
|
||||
this.options.logger.info(
|
||||
`Risk engine savedObject configuration: ${JSON.stringify(soResult, null, 2)}`
|
||||
);
|
||||
result.riskEngineConfigurationCreated = true;
|
||||
} catch (e) {
|
||||
result.errors.push(e.message);
|
||||
|
@ -319,4 +322,25 @@ export class RiskEngineDataClient {
|
|||
|
||||
return RiskEngineStatusEnum.ENABLED;
|
||||
}
|
||||
|
||||
public async updateRiskEngineSavedObject(attributes: {}) {
|
||||
try {
|
||||
const configuration = await this.getConfiguration();
|
||||
if (!configuration) {
|
||||
await initSavedObjects({
|
||||
savedObjectsClient: this.options.soClient,
|
||||
namespace: this.options.namespace,
|
||||
});
|
||||
}
|
||||
return await updateSavedObjectAttribute({
|
||||
savedObjectsClient: this.options.soClient,
|
||||
attributes,
|
||||
});
|
||||
} catch (e) {
|
||||
this.options.logger.error(
|
||||
`Error updating risk score engine saved object attributes: ${e.message}`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 {
|
||||
serverMock,
|
||||
requestContextMock,
|
||||
requestMock,
|
||||
} from '../../../detection_engine/routes/__mocks__';
|
||||
import { riskEnginePrivilegesMock } from './risk_engine_privileges.mock';
|
||||
import { riskEngineDataClientMock } from '../risk_engine_data_client.mock';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
import { RISK_ENGINE_CONFIGURE_SO_URL } from '../../../../../common/constants';
|
||||
import { riskEngineConfigureSavedObjectRoute } from './configure_saved_object';
|
||||
|
||||
describe('riskEnginConfigureSavedObjectRoute', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let context: ReturnType<typeof requestContextMock.convertContext>;
|
||||
let mockTaskManagerStart: ReturnType<typeof taskManagerMock.createStart>;
|
||||
let mockRiskEngineDataClient: ReturnType<typeof riskEngineDataClientMock.create>;
|
||||
let getStartServicesMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
server = serverMock.create();
|
||||
const { clients } = requestContextMock.createTools();
|
||||
mockRiskEngineDataClient = riskEngineDataClientMock.create();
|
||||
mockRiskEngineDataClient.updateRiskEngineSavedObject = jest.fn();
|
||||
context = requestContextMock.convertContext(
|
||||
requestContextMock.create({
|
||||
...clients,
|
||||
riskEngineDataClient: mockRiskEngineDataClient,
|
||||
})
|
||||
);
|
||||
mockTaskManagerStart = taskManagerMock.createStart();
|
||||
getStartServicesMock = jest.fn().mockResolvedValue([
|
||||
{},
|
||||
{
|
||||
taskManager: mockTaskManagerStart,
|
||||
security: riskEnginePrivilegesMock.createMockSecurityStartWithFullRiskEngineAccess(),
|
||||
},
|
||||
]);
|
||||
riskEngineConfigureSavedObjectRoute(server.router, getStartServicesMock);
|
||||
});
|
||||
|
||||
const buildRequest = (body: {}) => {
|
||||
return requestMock.create({
|
||||
method: 'put',
|
||||
path: RISK_ENGINE_CONFIGURE_SO_URL,
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
||||
it('should call the router with the correct route and handler', async () => {
|
||||
const request = buildRequest({});
|
||||
await server.inject(request, context);
|
||||
expect(mockRiskEngineDataClient.updateRiskEngineSavedObject).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns a 200 when the saved object is updated successfully', async () => {
|
||||
const request = buildRequest({
|
||||
exclude_alert_statuses: ['open'],
|
||||
range: { start: 'now-30d', end: 'now' },
|
||||
exclude_alert_tags: ['tag1'],
|
||||
});
|
||||
const response = await server.inject(request, context);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({ risk_engine_saved_object_configured: true });
|
||||
expect(mockRiskEngineDataClient.updateRiskEngineSavedObject).toHaveBeenCalledWith({
|
||||
excludeAlertStatuses: ['open'],
|
||||
range: { start: 'now-30d', end: 'now' },
|
||||
excludeAlertTags: ['tag1'],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import type { IKibanaResponse } from '@kbn/core-http-server';
|
||||
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
||||
import type { ConfigureRiskEngineSavedObjectResponse } from '../../../../../common/api/entity_analytics';
|
||||
import { ConfigureRiskEngineSavedObjectRequestBody } from '../../../../../common/api/entity_analytics';
|
||||
import {
|
||||
RISK_ENGINE_CONFIGURE_SO_URL,
|
||||
APP_ID,
|
||||
API_VERSIONS,
|
||||
} from '../../../../../common/constants';
|
||||
import { TASK_MANAGER_UNAVAILABLE_ERROR } from './translations';
|
||||
import { withRiskEnginePrivilegeCheck } from '../risk_engine_privileges';
|
||||
import type { EntityAnalyticsRoutesDeps } from '../../types';
|
||||
import { RiskEngineAuditActions } from '../audit';
|
||||
import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../../audit';
|
||||
|
||||
export const riskEngineConfigureSavedObjectRoute = (
|
||||
router: EntityAnalyticsRoutesDeps['router'],
|
||||
getStartServices: EntityAnalyticsRoutesDeps['getStartServices']
|
||||
) => {
|
||||
router.versioned
|
||||
.put({
|
||||
access: 'public',
|
||||
path: RISK_ENGINE_CONFIGURE_SO_URL,
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`],
|
||||
},
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {
|
||||
request: { body: buildRouteValidationWithZod(ConfigureRiskEngineSavedObjectRequestBody) },
|
||||
},
|
||||
},
|
||||
withRiskEnginePrivilegeCheck(
|
||||
getStartServices,
|
||||
async (
|
||||
context,
|
||||
request,
|
||||
response
|
||||
): Promise<IKibanaResponse<ConfigureRiskEngineSavedObjectResponse>> => {
|
||||
const securitySolution = await context.securitySolution;
|
||||
|
||||
securitySolution.getAuditLogger()?.log({
|
||||
message: 'User attempted to configure the saved object of the risk engine',
|
||||
event: {
|
||||
action: RiskEngineAuditActions.RISK_ENGINE_CONFIGURE_SAVED_OBJECT,
|
||||
category: AUDIT_CATEGORY.DATABASE,
|
||||
type: AUDIT_TYPE.CHANGE,
|
||||
outcome: AUDIT_OUTCOME.UNKNOWN,
|
||||
},
|
||||
});
|
||||
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
const [_, { taskManager }] = await getStartServices();
|
||||
const riskEngineClient = securitySolution.getRiskEngineDataClient();
|
||||
|
||||
if (!taskManager) {
|
||||
securitySolution.getAuditLogger()?.log({
|
||||
message:
|
||||
'User attempted to configure the saved object of the risk engine, but the Kibana Task Manager was unavailable',
|
||||
event: {
|
||||
action: RiskEngineAuditActions.RISK_ENGINE_CONFIGURE_SAVED_OBJECT,
|
||||
category: AUDIT_CATEGORY.DATABASE,
|
||||
type: AUDIT_TYPE.CHANGE,
|
||||
outcome: AUDIT_OUTCOME.FAILURE,
|
||||
},
|
||||
error: {
|
||||
message:
|
||||
'User attempted to configure the saved object of the risk engine, but the Kibana Task Manager was unavailable',
|
||||
},
|
||||
});
|
||||
|
||||
return siemResponse.error({
|
||||
statusCode: 400,
|
||||
body: TASK_MANAGER_UNAVAILABLE_ERROR,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await riskEngineClient.updateRiskEngineSavedObject({
|
||||
excludeAlertStatuses: request.body.exclude_alert_statuses,
|
||||
range: request.body.range,
|
||||
excludeAlertTags: request.body.exclude_alert_tags,
|
||||
});
|
||||
return response.ok({ body: { risk_engine_saved_object_configured: true } });
|
||||
} catch (e) {
|
||||
const error = transformError(e);
|
||||
|
||||
return siemResponse.error({
|
||||
statusCode: error.statusCode,
|
||||
body: { message: error.message, full_error: JSON.stringify(e) },
|
||||
bypassErrorFormat: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
|
@ -13,6 +13,7 @@ import { riskEngineSettingsRoute } from './settings';
|
|||
import type { EntityAnalyticsRoutesDeps } from '../../types';
|
||||
import { riskEngineScheduleNowRoute } from './schedule_now';
|
||||
import { riskEngineCleanupRoute } from './delete';
|
||||
import { riskEngineConfigureSavedObjectRoute } from './configure_saved_object';
|
||||
|
||||
export const registerRiskEngineRoutes = ({
|
||||
router,
|
||||
|
@ -26,4 +27,5 @@ export const registerRiskEngineRoutes = ({
|
|||
riskEngineSettingsRoute(router);
|
||||
riskEnginePrivilegesRoute(router, getStartServices);
|
||||
riskEngineCleanupRoute(router, getStartServices);
|
||||
riskEngineConfigureSavedObjectRoute(router, getStartServices);
|
||||
};
|
||||
|
|
|
@ -42,7 +42,10 @@ export const updateSavedObjectAttribute = async ({
|
|||
attributes,
|
||||
}: SavedObjectsClientArg & {
|
||||
attributes: {
|
||||
enabled: boolean;
|
||||
enabled?: boolean;
|
||||
excludeAlertIds?: string[];
|
||||
range?: { start: string; end: string };
|
||||
excludeAlertTags?: string[];
|
||||
};
|
||||
}) => {
|
||||
const savedObjectConfiguration = await getConfigurationSavedObject({
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server';
|
|||
import {
|
||||
ALERT_RISK_SCORE,
|
||||
ALERT_WORKFLOW_STATUS,
|
||||
ALERT_WORKFLOW_TAGS,
|
||||
} from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names';
|
||||
import type {
|
||||
AssetCriticalityRecord,
|
||||
|
@ -219,6 +220,7 @@ export const calculateRiskScores = async ({
|
|||
weights,
|
||||
alertSampleSizePerShard = 10_000,
|
||||
excludeAlertStatuses = [],
|
||||
excludeAlertTags = [],
|
||||
}: {
|
||||
assetCriticalityService: AssetCriticalityService;
|
||||
esClient: ElasticsearchClient;
|
||||
|
@ -236,6 +238,11 @@ export const calculateRiskScores = async ({
|
|||
if (!isEmpty(userFilter)) {
|
||||
filter.push(userFilter as QueryDslQueryContainer);
|
||||
}
|
||||
if (excludeAlertTags.length > 0) {
|
||||
filter.push({
|
||||
bool: { must_not: { terms: { [ALERT_WORKFLOW_TAGS]: excludeAlertTags } } },
|
||||
});
|
||||
}
|
||||
const identifierTypes: IdentifierType[] = identifierType ? [identifierType] : ['host', 'user'];
|
||||
const request = {
|
||||
size: 0,
|
||||
|
|
|
@ -250,5 +250,35 @@ describe('POST risk_engine/preview route', () => {
|
|||
expect(result.ok).toHaveBeenCalledWith(expect.objectContaining({ after_keys: {} }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('exclude_alert_statuses', () => {
|
||||
it('respects the provided exclude_alert_statuses', async () => {
|
||||
const request = buildRequest({
|
||||
exclude_alert_statuses: ['open'],
|
||||
});
|
||||
|
||||
const response = await server.inject(request, requestContextMock.convertContext(context));
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(mockRiskScoreService.calculateScores).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ excludeAlertStatuses: ['open'] })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exclude_alert_tags', () => {
|
||||
it('respects the provided exclude_alert_tags', async () => {
|
||||
const request = buildRequest({
|
||||
exclude_alert_tags: ['tag1'],
|
||||
});
|
||||
|
||||
const response = await server.inject(request, requestContextMock.convertContext(context));
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(mockRiskScoreService.calculateScores).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ excludeAlertTags: ['tag1'] })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -65,7 +65,8 @@ export const riskScorePreviewRoute = (
|
|||
filter,
|
||||
range: userRange,
|
||||
weights,
|
||||
excludeAlertStatuses,
|
||||
exclude_alert_statuses: excludedStatuses,
|
||||
exclude_alert_tags: excludedTags,
|
||||
} = request.body;
|
||||
|
||||
const entityAnalyticsConfig = await riskScoreService.getConfigurationWithDefaults(
|
||||
|
@ -84,6 +85,8 @@ export const riskScorePreviewRoute = (
|
|||
const afterKeys = userAfterKeys ?? {};
|
||||
const range = userRange ?? { start: 'now-15d', end: 'now' };
|
||||
const pageSize = userPageSize ?? DEFAULT_RISK_SCORE_PAGE_SIZE;
|
||||
const excludeAlertStatuses = excludedStatuses || ['closed'];
|
||||
const excludeAlertTags = excludedTags || [];
|
||||
|
||||
const result = await riskScoreService.calculateScores({
|
||||
afterKeys,
|
||||
|
@ -97,6 +100,7 @@ export const riskScorePreviewRoute = (
|
|||
weights,
|
||||
alertSampleSizePerShard,
|
||||
excludeAlertStatuses,
|
||||
excludeAlertTags,
|
||||
});
|
||||
|
||||
securityContext.getAuditLogger()?.log({
|
||||
|
|
|
@ -257,6 +257,7 @@ export const runTask = async ({
|
|||
const configuration = await riskScoreService.getConfigurationWithDefaults(
|
||||
entityAnalyticsConfig
|
||||
);
|
||||
log(`Risk engine running with configuration : ${JSON.stringify(configuration, null, 2)}`);
|
||||
if (configuration == null) {
|
||||
log(
|
||||
'Risk engine configuration not found; exiting task. Please reinitialize the risk engine and try again'
|
||||
|
|
|
@ -86,6 +86,7 @@ export interface CalculateScoresParams {
|
|||
weights?: RiskScoreWeights;
|
||||
alertSampleSizePerShard?: number;
|
||||
excludeAlertStatuses?: string[];
|
||||
excludeAlertTags?: string[];
|
||||
}
|
||||
|
||||
export interface CalculateAndPersistScoresParams {
|
||||
|
|
|
@ -28,6 +28,7 @@ import { BulkPatchRulesRequestBodyInput } from '@kbn/security-solution-plugin/co
|
|||
import { BulkUpdateRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_update_rules_route.gen';
|
||||
import { BulkUpsertAssetCriticalityRecordsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.gen';
|
||||
import { CleanDraftTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/clean_draft_timelines/clean_draft_timelines_route.gen';
|
||||
import { ConfigureRiskEngineSavedObjectRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/engine_configure_saved_object_route.gen';
|
||||
import { CopyTimelineRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/copy_timeline/copy_timeline_route.gen';
|
||||
import { CreateAlertsMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/create_signals_migration/create_signals_migration.gen';
|
||||
import { CreateAssetCriticalityRecordRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/create_asset_criticality.gen';
|
||||
|
@ -314,6 +315,20 @@ If asset criticality records already exist for the specified entities, those rec
|
|||
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
|
||||
},
|
||||
/**
|
||||
* Configuring the Risk Engine Saved Object
|
||||
*/
|
||||
configureRiskEngineSavedObject(
|
||||
props: ConfigureRiskEngineSavedObjectProps,
|
||||
kibanaSpace: string = 'default'
|
||||
) {
|
||||
return supertest
|
||||
.patch(routeWithNamespace('/api/risk_score/engine/saved_object/configure', kibanaSpace))
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.send(props.body as object);
|
||||
},
|
||||
/**
|
||||
* Copies and returns a timeline or timeline template.
|
||||
|
||||
|
@ -1634,6 +1649,9 @@ export interface BulkUpsertAssetCriticalityRecordsProps {
|
|||
export interface CleanDraftTimelinesProps {
|
||||
body: CleanDraftTimelinesRequestBodyInput;
|
||||
}
|
||||
export interface ConfigureRiskEngineSavedObjectProps {
|
||||
body: ConfigureRiskEngineSavedObjectRequestBodyInput;
|
||||
}
|
||||
export interface CopyTimelineProps {
|
||||
body: CopyTimelineRequestBodyInput;
|
||||
}
|
||||
|
|
|
@ -21,5 +21,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./asset_criticality_csv_upload'));
|
||||
loadTestFile(require.resolve('./risk_score_entity_calculation'));
|
||||
loadTestFile(require.resolve('./risk_engine_schedule_now'));
|
||||
loadTestFile(require.resolve('./risk_engine_so_config'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { riskEngineConfigurationTypeName } from '@kbn/security-solution-plugin/server/lib/entity_analytics/risk_engine/saved_object';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
import {
|
||||
riskEngineRouteHelpersFactory,
|
||||
getRiskEngineConfigSO,
|
||||
waitForRiskEngineRun,
|
||||
waitForRiskEngineTaskToBeGone,
|
||||
} from '../../utils';
|
||||
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const spaceName = 'space1';
|
||||
const supertest = getService('supertest');
|
||||
const riskEngineRoutes = riskEngineRouteHelpersFactory(supertest);
|
||||
const riskEngineRoutesForNamespace = riskEngineRouteHelpersFactory(supertest, spaceName);
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('@ess @ serverless @serverless QA risk_engine_so_update_config', () => {
|
||||
before(async () => {
|
||||
const soId = await kibanaServer.savedObjects.find({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
space: spaceName,
|
||||
});
|
||||
if (soId.saved_objects.length !== 0) {
|
||||
await kibanaServer.savedObjects.delete({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
space: spaceName,
|
||||
id: soId.saved_objects[0].id,
|
||||
});
|
||||
}
|
||||
const soId2 = await kibanaServer.savedObjects.find({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
});
|
||||
if (soId2.saved_objects.length !== 0) {
|
||||
await kibanaServer.savedObjects.delete({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
id: soId2.saved_objects[0].id,
|
||||
});
|
||||
}
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/security_solution/ecs_compliant');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
const soId = await kibanaServer.savedObjects.find({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
space: spaceName,
|
||||
});
|
||||
if (soId.saved_objects.length !== 0) {
|
||||
await kibanaServer.savedObjects.delete({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
space: spaceName,
|
||||
id: soId.saved_objects[0].id,
|
||||
});
|
||||
}
|
||||
const soId2 = await kibanaServer.savedObjects.find({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
});
|
||||
if (soId2.saved_objects.length !== 0) {
|
||||
await kibanaServer.savedObjects.delete({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
id: soId2.saved_objects[0].id,
|
||||
});
|
||||
}
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/ecs_compliant');
|
||||
});
|
||||
|
||||
it('should include the right keys as per the update', async () => {
|
||||
await riskEngineRoutes.init();
|
||||
await waitForRiskEngineRun;
|
||||
|
||||
const currentSoConfig = await getRiskEngineConfigSO({ kibanaServer });
|
||||
|
||||
expect(currentSoConfig.attributes).to.not.have.property('excludeAlertTags');
|
||||
expect(currentSoConfig.attributes).to.not.have.property('excludeAlertStatuses');
|
||||
|
||||
const updatedSoBody = {
|
||||
exclude_alert_tags: ['False Positive'],
|
||||
exclude_alert_statuses: ['open'],
|
||||
};
|
||||
|
||||
await riskEngineRoutes.soConfig(updatedSoBody, 200);
|
||||
const currentSoConfig2 = await getRiskEngineConfigSO({ kibanaServer });
|
||||
|
||||
expect(currentSoConfig2.attributes).to.have.property('excludeAlertTags');
|
||||
expect(currentSoConfig2.attributes).to.have.property('excludeAlertStatuses');
|
||||
|
||||
await riskEngineRoutes.disable();
|
||||
await waitForRiskEngineTaskToBeGone;
|
||||
|
||||
updatedSoBody.exclude_alert_statuses = [];
|
||||
|
||||
await riskEngineRoutes.soConfig(updatedSoBody, 200);
|
||||
|
||||
await riskEngineRoutes.enable();
|
||||
await waitForRiskEngineRun;
|
||||
|
||||
const currentSoConfig3 = await getRiskEngineConfigSO({ kibanaServer });
|
||||
expect(JSON.stringify(currentSoConfig3.attributes.excludeAlertStatuses)).to.equal(
|
||||
JSON.stringify(updatedSoBody.exclude_alert_statuses)
|
||||
);
|
||||
});
|
||||
|
||||
it('should succeed while updating the saved object', async () => {
|
||||
await riskEngineRoutes.init();
|
||||
await waitForRiskEngineRun;
|
||||
|
||||
const updatedSoBody = {
|
||||
exclude_alert_tags: ['False Positive'],
|
||||
exclude_alert_statuses: ['open'],
|
||||
};
|
||||
const response = await riskEngineRoutes.soConfig(updatedSoBody);
|
||||
expect(response.status).to.equal(200);
|
||||
});
|
||||
|
||||
it('should update the config in the right space', async () => {
|
||||
await riskEngineRoutesForNamespace.init();
|
||||
await riskEngineRoutes.init();
|
||||
await waitForRiskEngineRun;
|
||||
|
||||
const updatedSoBody = {
|
||||
exclude_alert_tags: ['False Positive'],
|
||||
exclude_alert_statuses: ['open', 'closed'],
|
||||
};
|
||||
|
||||
await riskEngineRoutesForNamespace.soConfig(updatedSoBody, 200);
|
||||
const currentSoConfig = await getRiskEngineConfigSO({ kibanaServer, space: 'space1' });
|
||||
|
||||
expect(currentSoConfig.namespaces).to.eql(['space1']);
|
||||
expect(currentSoConfig.attributes.excludeAlertTags).to.eql(updatedSoBody.exclude_alert_tags);
|
||||
expect(currentSoConfig.attributes.excludeAlertStatuses).to.eql(
|
||||
updatedSoBody.exclude_alert_statuses
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -24,6 +24,7 @@ import {
|
|||
RISK_ENGINE_PRIVILEGES_URL,
|
||||
RISK_ENGINE_CLEANUP_URL,
|
||||
RISK_ENGINE_SCHEDULE_NOW_URL,
|
||||
RISK_ENGINE_CONFIGURE_SO_URL,
|
||||
} from '@kbn/security-solution-plugin/common/constants';
|
||||
import { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { removeLegacyTransforms } from '@kbn/security-solution-plugin/server/lib/entity_analytics/utils/transforms';
|
||||
|
@ -365,9 +366,16 @@ export const waitForRiskScoresToBeGone = async ({
|
|||
);
|
||||
};
|
||||
|
||||
export const getRiskEngineConfigSO = async ({ kibanaServer }: { kibanaServer: KbnClient }) => {
|
||||
export const getRiskEngineConfigSO = async ({
|
||||
kibanaServer,
|
||||
space,
|
||||
}: {
|
||||
kibanaServer: KbnClient;
|
||||
space?: string;
|
||||
}) => {
|
||||
const soResponse = await kibanaServer.savedObjects.find({
|
||||
type: riskEngineConfigurationTypeName,
|
||||
space,
|
||||
});
|
||||
|
||||
return soResponse?.saved_objects?.[0];
|
||||
|
@ -580,6 +588,17 @@ export const riskEngineRouteHelpersFactory = (supertest: SuperTest.Agent, namesp
|
|||
assertStatusCode(expectStatusCode, response);
|
||||
return response;
|
||||
},
|
||||
|
||||
soConfig: async (configParams: {}, expectStatusCode: number = 200) => {
|
||||
const response = await supertest
|
||||
.put(routeWithNamespace(RISK_ENGINE_CONFIGURE_SO_URL, namespace))
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
||||
.send(configParams);
|
||||
assertStatusCode(expectStatusCode, response);
|
||||
return response;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -7,15 +7,9 @@
|
|||
|
||||
import {
|
||||
PAGE_TITLE,
|
||||
HOST_RISK_PREVIEW_TABLE,
|
||||
HOST_RISK_PREVIEW_TABLE_ROWS,
|
||||
USER_RISK_PREVIEW_TABLE,
|
||||
USER_RISK_PREVIEW_TABLE_ROWS,
|
||||
RISK_PREVIEW_ERROR,
|
||||
LOCAL_QUERY_BAR_SELECTOR,
|
||||
RISK_SCORE_ERROR_PANEL,
|
||||
RISK_SCORE_STATUS,
|
||||
LOCAL_QUERY_BAR_SEARCH_INPUT_SELECTOR,
|
||||
} from '../../screens/entity_analytics_management';
|
||||
|
||||
import { deleteRiskScore, installRiskScoreModule } from '../../tasks/api_calls/risk_scores';
|
||||
|
@ -31,8 +25,6 @@ import {
|
|||
interceptRiskPreviewSuccess,
|
||||
interceptRiskInitError,
|
||||
} from '../../tasks/api_calls/risk_engine';
|
||||
import { updateDateRangeInLocalDatePickers } from '../../tasks/date_picker';
|
||||
import { submitLocalSearch } from '../../tasks/search_bar';
|
||||
import {
|
||||
riskEngineStatusChange,
|
||||
upgradeRiskEngine,
|
||||
|
@ -65,31 +57,6 @@ describe(
|
|||
});
|
||||
|
||||
describe('Risk preview', () => {
|
||||
it('risk scores reacts on change in datepicker', () => {
|
||||
const START_DATE = 'Jan 18, 2019 @ 20:33:29.186';
|
||||
const END_DATE = 'Jan 19, 2019 @ 20:33:29.186';
|
||||
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
|
||||
updateDateRangeInLocalDatePickers(LOCAL_QUERY_BAR_SELECTOR, START_DATE, END_DATE);
|
||||
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE).contains('No items found');
|
||||
cy.get(USER_RISK_PREVIEW_TABLE).contains('No items found');
|
||||
});
|
||||
|
||||
it('risk scores reacts on change in search bar query', () => {
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
cy.get(LOCAL_QUERY_BAR_SEARCH_INPUT_SELECTOR).type('host.name: "test-host1"');
|
||||
submitLocalSearch(LOCAL_QUERY_BAR_SELECTOR);
|
||||
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 1);
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).contains('test-host1');
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 1);
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).contains('test1');
|
||||
});
|
||||
|
||||
it('show error panel if API returns error and then try to refetch data', () => {
|
||||
interceptRiskPreviewError();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue