[Security Solution][Risk Score] Use Risk Engine SavedObject intead of localStorage on the Risk Score web page (#215304)

## Summary

The PR updates the implementation to fetch data from the Risk Engine
Saved Object instead of storing and reusing it from LocalStorage.

This change ensures that settings are applied globally rather than being
limited to the browser’s LocalStorage. Since the Saved Object holds the
most up-to-date information, it is now used to update the "Date" and the
toggle for "including closed alerts for risk scoring" across all web
browsers.


### Normal and Incognito Mode : 



https://github.com/user-attachments/assets/7638c88b-ff9e-4d42-9944-e55b53e33518


### Default space vs custom space : 



https://github.com/user-attachments/assets/46bb35c7-3cd9-4b97-9f1c-90ec4ef1241a


## Testing Steps

### Verify Initial Values
1. Open the Entity Risk Score web page where the settings are applied.
2. Ensure that the date picker and toggle for "including closed alerts"
reflect the values stored in the Risk Engine Saved Object rather than
LocalStorage.
3. Modify and Save changes,
   - Change the date range in the date picker.
   - Toggle the "Include Closed Alerts" switch.

### Page Refresh Test
- Refresh the page and confirm that the modified values persist, fetched
correctly from the Risk Engine Saved Object.

### Cross-Browser Test
- Open the same web page in a different browser or incognito mode.
- Verify that the settings are consistent and correctly loaded from the
Risk Engine Saved
  Object.

### Expected Outcome
The settings should persist after a page refresh or across different
browsers.
The latest values should always be pulled from the Risk Engine Saved
Object.


### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Abhishek Bhatia 2025-03-21 14:19:24 +05:30 committed by GitHub
parent 933564d713
commit dbe28b9f94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 50 additions and 101 deletions

View file

@ -21,4 +21,8 @@ import { DateRange } from '../common/common.gen';
export type ReadRiskEngineSettingsResponse = z.infer<typeof ReadRiskEngineSettingsResponse>;
export const ReadRiskEngineSettingsResponse = z.object({
range: DateRange.optional(),
/**
* Include closed alerts in the risk score calculation
*/
includeClosedAlerts: z.boolean().optional(),
});

View file

@ -21,3 +21,6 @@ paths:
properties:
range:
$ref: '../common/common.schema.yaml#/components/schemas/DateRange'
includeClosedAlerts:
type: boolean
description: Include closed alerts in the risk score calculation

View file

@ -22,10 +22,8 @@ describe('RiskScoreConfigurationSection', () => {
const mockConfigureSO = useConfigureSORiskEngineMutation as jest.Mock;
const defaultProps = {
includeClosedAlerts: false,
setIncludeClosedAlerts: jest.fn(),
from: 'now-30d',
to: 'now',
onDateChange: jest.fn(),
};
const mockAddSuccess = jest.fn();
@ -50,13 +48,17 @@ describe('RiskScoreConfigurationSection', () => {
<RiskScoreConfigurationSection {...defaultProps} includeClosedAlerts={true} />
);
wrapper.find(EuiSwitch).simulate('click');
expect(defaultProps.setIncludeClosedAlerts).toHaveBeenCalledWith(true);
expect(wrapper.find(EuiSwitch).prop('checked')).toBe(true);
act(() => {
wrapper.find(EuiSwitch).simulate('click');
});
wrapper.update();
expect(wrapper.find(EuiSwitch).prop('checked')).toBe(true);
});
it('calls onDateChange on date change', () => {
const wrapper = mount(<RiskScoreConfigurationSection {...defaultProps} />);
wrapper.find(EuiSuperDatePicker).props().onTimeChange({ start: 'now-30d', end: 'now' });
expect(defaultProps.onDateChange).toHaveBeenCalledWith({ start: 'now-30d', end: 'now' });
});
it('shows bottom bar when changes are made', async () => {
@ -71,20 +73,20 @@ describe('RiskScoreConfigurationSection', () => {
});
it('saves changes', () => {
const wrapper = mount(
<RiskScoreConfigurationSection {...defaultProps} includeClosedAlerts={true} />
);
const wrapper = mount(<RiskScoreConfigurationSection {...defaultProps} />);
// Simulate clicking the switch
// Simulate clicking the toggle switch
const closedAlertsToggle = wrapper.find('button[data-test-subj="includeClosedAlertsSwitch"]');
expect(closedAlertsToggle.exists()).toBe(true);
closedAlertsToggle.simulate('click');
wrapper.update();
// Simulate clicking the save button in the bottom bar
const saveChangesButton = wrapper.find('button[data-test-subj="riskScoreSaveButton"]');
expect(saveChangesButton.exists()).toBe(true);
saveChangesButton.simulate('click');
wrapper.update();
const callArgs = mockMutate.mock.calls[0][0];
expect(callArgs).toEqual({
includeClosedAlerts: true,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect } from 'react';
import {
EuiSuperDatePicker,
EuiButton,
@ -18,7 +18,6 @@ import {
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';
@ -26,87 +25,45 @@ 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 [checked, setChecked] = useState(includeClosedAlerts);
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
);
const [start, setStart] = useState(from);
const [end, setEnd] = useState(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]);
setChecked(includeClosedAlerts);
setStart(from);
setEnd(to);
}, [includeClosedAlerts, from, to]);
const onRefresh = ({ start: newStart, end: newEnd }: { start: string; end: string }) => {
setFrom(newStart);
const adjustedEnd = newStart === newEnd ? 'now' : newEnd;
setTo(adjustedEnd);
onDateChange({ start: newStart, end: adjustedEnd });
checkForChanges(newStart, adjustedEnd, includeClosedAlerts);
const handleDateChange = ({ start: newStart, end: newEnd }: { start: string; end: string }) => {
setShowBar(true);
setStart(newStart);
setEnd(newEnd);
};
const handleToggle = () => {
const newValue = !includeClosedAlerts;
setIncludeClosedAlerts(newValue);
checkForChanges(start, end, newValue);
const onChange = () => {
setChecked((prev) => !prev);
setShowBar(true);
};
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,
includeClosedAlerts: checked,
range: { start, end },
},
{
@ -116,14 +73,6 @@ export const RiskScoreConfigurationSection = ({
toastLifeTimeMs: 5000,
});
setIsLoading(false);
initialStart.current = start;
initialEnd.current = end;
initialIncludeClosedAlerts.current = includeClosedAlerts;
setSavedIncludeClosedAlerts(includeClosedAlerts);
setSavedStart(start);
setSavedEnd(end);
},
onError: () => {
setIsLoading(false);
@ -138,8 +87,8 @@ export const RiskScoreConfigurationSection = ({
<div>
<EuiSwitch
label={i18n.INCLUDE_CLOSED_ALERTS_LABEL}
checked={includeClosedAlerts}
onChange={handleToggle}
checked={checked}
onChange={onChange}
data-test-subj="includeClosedAlertsSwitch"
/>
</div>
@ -148,7 +97,7 @@ export const RiskScoreConfigurationSection = ({
<EuiSuperDatePicker
start={start}
end={end}
onTimeChange={onRefresh}
onTimeChange={handleDateChange}
width={'auto'}
compressed={false}
showUpdateButton={false}
@ -170,9 +119,9 @@ export const RiskScoreConfigurationSection = ({
iconType="cross"
onClick={() => {
setShowBar(false);
setFrom(initialStart.current);
setTo(initialEnd.current);
setIncludeClosedAlerts(initialIncludeClosedAlerts.current);
setStart(start);
setEnd(end);
setChecked(includeClosedAlerts);
}}
>
{i18n.DISCARD_CHANGES}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState, useCallback } from 'react';
import React, { useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@ -28,6 +28,7 @@ import { useScheduleNowRiskEngineMutation } from '../api/hooks/use_schedule_now_
import { useAppToasts } from '../../common/hooks/use_app_toasts';
import * as i18n from '../translations';
import { getEntityAnalyticsRiskScorePageStyles } from '../components/risk_score_page_styles';
import { useRiskEngineSettings } from '../api/hooks/use_risk_engine_settings';
const TEN_SECONDS = 10000;
@ -35,9 +36,10 @@ 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-30d');
const [to, setTo] = useState(localStorage.getItem('dateEnd') || 'now');
const { data: riskEngineSettings } = useRiskEngineSettings();
const includeClosedAlerts = riskEngineSettings?.includeClosedAlerts ?? false;
const from = riskEngineSettings?.range?.start ?? 'now-30d';
const to = riskEngineSettings?.range?.end || 'now';
const { data: riskEngineStatus } = useRiskEngineStatus({
refetchInterval: TEN_SECONDS,
structuralSharing: false, // Force the component to rerender after every Risk Engine Status API call
@ -65,20 +67,6 @@ export const EntityAnalyticsManagementPage = () => {
}
};
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());
@ -148,10 +136,8 @@ export const EntityAnalyticsManagementPage = () => {
<EuiFlexItem grow={2}>
<RiskScoreConfigurationSection
includeClosedAlerts={includeClosedAlerts}
setIncludeClosedAlerts={handleIncludeClosedAlertsToggle}
from={from}
to={to}
onDateChange={handleDateChange}
/>
<EuiHorizontalRule />
<RiskScoreUsefulLinksSection />

View file

@ -55,6 +55,9 @@ export const riskEngineSettingsRoute = (router: EntityAnalyticsRoutesDeps['route
return response.ok({
body: {
range: result.range,
includeClosedAlerts:
Array.isArray(result?.excludeAlertStatuses) &&
!result.excludeAlertStatuses.includes('closed'),
},
});
} catch (e) {

View file

@ -81,6 +81,8 @@ export interface RiskEngineConfiguration {
_meta: {
mappingsVersion: number;
};
excludeAlertStatuses?: string[];
excludeAlertTags?: string[];
}
export interface CalculateScoresParams {