[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.


![Image](https://github.com/user-attachments/assets/5f91c990-22d6-46e5-8a7b-9875003867e4)

### **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:
Abhishek Bhatia 2024-12-13 12:11:12 +05:30 committed by GitHub
parent a9f076cb1f
commit a95ec61444
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1615 additions and 164 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -86,6 +86,7 @@ export interface CalculateScoresParams {
weights?: RiskScoreWeights;
alertSampleSizePerShard?: number;
excludeAlertStatuses?: string[];
excludeAlertTags?: string[];
}
export interface CalculateAndPersistScoresParams {

View file

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

View file

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

View file

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

View file

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

View file

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