[Cloud Security] Handle Detection Rules and Benchmark rules errors (#191150)

## Summary

This PR addresses the following:

- Handling errors when creating Detection rules
- Handling errors when muting/unmuting Benchmark rules
- Handling Alerts pending on loading state when the user doesn't have
read permission to the Detection Rules alerts index
(`.alerts-security.alerts-default`).
- Centralized benchmark rules mutation success/error logic in the
`useChangeCspRuleState` hook.
- Fixed benchmark mute API that was disabling only one detection rule
per benchmark rule.


## Recordings

### Read Only User (Without .alerts-security.alerts-default read
permission)


https://github.com/user-attachments/assets/bbf153f8-2e33-4449-a25e-7bdf52e45275

### Read Only User (With .alerts-security.alerts-default read
permission)


https://github.com/user-attachments/assets/c4b69043-ab97-485b-8fdb-17245c44d48b

### Write permission user


https://github.com/user-attachments/assets/954c8b5e-0046-4e7e-a401-787e1b7eb211

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Paulo Henrique 2024-08-23 15:44:07 -07:00 committed by GitHub
parent 8ba84ecf00
commit 259518214a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 297 additions and 291 deletions

View file

@ -34,5 +34,7 @@ export const useFetchDetectionRulesAlertsStatus = (tags: string[]) => {
version: DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION,
query: { tags },
}),
// Disabling retry to prevent stuck on loading state when the request fails due to permissions
retry: false,
});
};

View file

@ -21,7 +21,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount';
import type { HttpSetup } from '@kbn/core/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n as kbnI18n } from '@kbn/i18n';
import { QueryClient, useQueryClient } from '@tanstack/react-query';
import { QueryClient, useMutation, useQueryClient } from '@tanstack/react-query';
import type { RuleResponse } from '../common/types';
import { CREATE_RULE_ACTION_SUBJ, TAKE_ACTION_SUBJ } from './test_subjects';
import { useKibana } from '../common/hooks/use_kibana';
@ -38,6 +38,22 @@ interface TakeActionProps {
isDataGridControlColumn?: boolean;
}
export const showCreateDetectionRuleErrorToast = (
cloudSecurityStartServices: CloudSecurityPostureStartServices,
error: Error
) => {
return cloudSecurityStartServices.notifications.toasts.addDanger({
title: kbnI18n.translate('xpack.csp.takeAction.createRuleErrorTitle', {
defaultMessage: 'Unable to create detection rule',
}),
text: kbnI18n.translate('xpack.csp.takeAction.createRuleErrorDescription', {
defaultMessage: 'An error occurred while creating the detection rule: {errorMessage}.',
values: { errorMessage: error.message },
}),
'data-test-subj': 'csp:toast-error',
});
};
export const showCreateDetectionRuleSuccessToast = (
cloudSecurityStartServices: CloudSecurityPostureStartServices,
http: HttpSetup,
@ -92,78 +108,6 @@ export const showCreateDetectionRuleSuccessToast = (
});
};
export const showChangeBenchmarkRuleStatesSuccessToast = (
cloudSecurityStartServices: CloudSecurityPostureStartServices,
isBenchmarkRuleMuted: boolean,
data: {
numberOfRules: number;
numberOfDetectionRules: number;
}
) => {
const { notifications, analytics, i18n, theme } = cloudSecurityStartServices;
const startServices = { analytics, i18n, theme };
return notifications.toasts.addSuccess({
toastLifeTimeMs: 10000,
color: 'success',
iconType: '',
'data-test-subj': 'csp:toast-success-rule-state-change',
title: toMountPoint(
<EuiText size="m">
<strong data-test-subj={`csp:toast-success-rule-title`}>
{isBenchmarkRuleMuted ? (
<FormattedMessage
id="xpack.csp.flyout.ruleEnabledToastTitle"
defaultMessage="Rule Enabled"
/>
) : (
<FormattedMessage
id="xpack.csp.flyout.ruleDisabledToastTitle"
defaultMessage="Rule Disabled"
/>
)}
</strong>
</EuiText>,
startServices
),
text: toMountPoint(
<div>
{isBenchmarkRuleMuted ? (
<FormattedMessage
id="xpack.csp.flyout.ruleEnabledToastRulesCount"
defaultMessage="Successfully enabled {ruleCount, plural, one {# rule} other {# rules}} "
values={{
ruleCount: data.numberOfRules,
}}
/>
) : (
<>
<FormattedMessage
id="xpack.csp.flyout.ruleDisabledToastRulesCount"
defaultMessage="Successfully disabled {ruleCount, plural, one {# rule} other {# rules}} "
values={{
ruleCount: data.numberOfRules,
}}
/>
{!isBenchmarkRuleMuted && data.numberOfDetectionRules > 0 && (
<strong>
<FormattedMessage
id="xpack.csp.flyout.ruleDisabledToastDetectionRulesCount"
defaultMessage=" and {detectionRuleCount, plural, one {# detection rule} other {# detection rules}}"
values={{
detectionRuleCount: data.numberOfDetectionRules,
}}
/>
</strong>
)}
</>
)}
</div>,
startServices
),
});
};
/*
* This component is used to create a detection rule from Flyout.
* It accepts a createRuleFn parameter which is used to create a rule in a generic way.
@ -269,20 +213,33 @@ const CreateDetectionRule = ({
}) => {
const { http, ...startServices } = useKibana().services;
const { mutate } = useMutation({
mutationFn: () => {
return createRuleFn(http);
},
onMutate: () => {
setIsLoading(true);
closePopover();
},
onSuccess: (ruleResponse) => {
showCreateDetectionRuleSuccessToast(startServices, http, ruleResponse);
// Triggering a refetch of rules and alerts to update the UI
queryClient.invalidateQueries([DETECTION_ENGINE_RULES_KEY]);
queryClient.invalidateQueries([DETECTION_ENGINE_ALERTS_KEY]);
},
onError: (error: Error) => {
showCreateDetectionRuleErrorToast(startServices, error);
},
onSettled: () => {
setIsLoading(false);
},
});
return (
<EuiContextMenuItem
key="createRule"
disabled={isCreateDetectionRuleDisabled}
onClick={async () => {
closePopover();
setIsLoading(true);
const ruleResponse = await createRuleFn(http);
setIsLoading(false);
showCreateDetectionRuleSuccessToast(startServices, http, ruleResponse);
// Triggering a refetch of rules and alerts to update the UI
queryClient.invalidateQueries([DETECTION_ENGINE_RULES_KEY]);
queryClient.invalidateQueries([DETECTION_ENGINE_ALERTS_KEY]);
}}
onClick={() => mutate()}
data-test-subj={CREATE_RULE_ACTION_SUBJ}
>
<FormattedMessage

View file

@ -23,19 +23,13 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { HttpSetup } from '@kbn/core/public';
import { useKibana } from '../../common/hooks/use_kibana';
import { getFindingsDetectionRuleSearchTags } from '../../../common/utils/detection_rules';
import { CspBenchmarkRuleMetadata } from '../../../common/types/latest';
import { getRuleList } from '../configurations/findings_flyout/rule_tab';
import { getRemediationList } from '../configurations/findings_flyout/overview_tab';
import * as TEST_SUBJECTS from './test_subjects';
import { useChangeCspRuleState } from './use_change_csp_rule_state';
import { CspBenchmarkRulesWithStates } from './rules_container';
import {
showChangeBenchmarkRuleStatesSuccessToast,
TakeAction,
} from '../../components/take_action';
import { useFetchDetectionRulesByTags } from '../../common/api/use_fetch_detection_rules_by_tags';
import { TakeAction } from '../../components/take_action';
import { createDetectionRuleFromBenchmarkRule } from '../configurations/utils/create_detection_rule_from_benchmark';
export const RULES_FLYOUT_SWITCH_BUTTON = 'rule-flyout-switch-button';
@ -66,13 +60,9 @@ type RuleTab = (typeof tabs)[number]['id'];
export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => {
const [tab, setTab] = useState<RuleTab>('overview');
const { mutate: mutateRuleState } = useChangeCspRuleState();
const { data: rulesData } = useFetchDetectionRulesByTags(
getFindingsDetectionRuleSearchTags(rule.metadata)
);
const { notifications, analytics, i18n: i18nStart, theme } = useKibana().services;
const startServices = { notifications, analytics, i18n: i18nStart, theme };
const isRuleMuted = rule?.state === 'muted';
const { mutate: mutateRuleState } = useChangeCspRuleState();
const switchRuleStates = async () => {
if (rule.metadata.benchmark.rule_number) {
@ -83,14 +73,10 @@ export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => {
rule_id: rule.metadata.id,
};
const nextRuleStates = isRuleMuted ? 'unmute' : 'mute';
await mutateRuleState({
mutateRuleState({
newState: nextRuleStates,
ruleIds: [rulesObjectRequest],
});
showChangeBenchmarkRuleStatesSuccessToast(startServices, isRuleMuted, {
numberOfRules: 1,
numberOfDetectionRules: rulesData?.total || 0,
});
}
};

View file

@ -20,16 +20,10 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { uniqBy } from 'lodash';
import { HttpSetup } from '@kbn/core/public';
import { CloudSecurityPostureStartServices } from '../../types';
import { useKibana } from '../../common/hooks/use_kibana';
import { getFindingsDetectionRuleSearchTags } from '../../../common/utils/detection_rules';
import { ColumnNameWithTooltip } from '../../components/column_name_with_tooltip';
import type { CspBenchmarkRulesWithStates, RulesState } from './rules_container';
import * as TEST_SUBJECTS from './test_subjects';
import { RuleStateUpdateRequest, useChangeCspRuleState } from './use_change_csp_rule_state';
import { showChangeBenchmarkRuleStatesSuccessToast } from '../../components/take_action';
import { fetchDetectionRulesByTags } from '../../common/api/use_fetch_detection_rules_by_tags';
import { useChangeCspRuleState } from './use_change_csp_rule_state';
export const RULES_ROWS_ENABLE_SWITCH_BUTTON = 'rules-row-enable-switch-button';
export const RULES_ROW_SELECT_ALL_CURRENT_PAGE = 'cloud-security-fields-selector-item-all';
@ -50,7 +44,6 @@ type GetColumnProps = Pick<
RulesTableProps,
'onRuleClick' | 'selectedRules' | 'setSelectedRules'
> & {
mutateRulesStates: (ruleStateUpdateRequest: RuleStateUpdateRequest) => void;
items: CspBenchmarkRulesWithStates[];
setIsAllRulesSelectedThisPage: (isAllRulesSelected: boolean) => void;
isAllRulesSelectedThisPage: boolean;
@ -58,8 +51,6 @@ type GetColumnProps = Pick<
currentPageRulesArray: CspBenchmarkRulesWithStates[],
selectedRulesArray: CspBenchmarkRulesWithStates[]
) => boolean;
http: HttpSetup;
startServices: CloudSecurityPostureStartServices;
};
export const RulesTable = ({
@ -111,8 +102,6 @@ export const RulesTable = ({
const [isAllRulesSelectedThisPage, setIsAllRulesSelectedThisPage] = useState<boolean>(false);
const { mutate: mutateRulesStates } = useChangeCspRuleState();
const isCurrentPageRulesASubset = (
currentPageRulesArray: CspBenchmarkRulesWithStates[],
selectedRulesArray: CspBenchmarkRulesWithStates[]
@ -128,16 +117,13 @@ export const RulesTable = ({
return true;
};
const { http, notifications, analytics, i18n: i18nStart, theme } = useKibana().services;
useEffect(() => {
if (selectedRules.length >= items.length && items.length > 0 && selectedRules.length > 0)
setIsAllRulesSelectedThisPage(true);
else setIsAllRulesSelectedThisPage(false);
}, [items.length, selectedRules.length]);
const startServices = { notifications, analytics, i18n: i18nStart, theme };
const columns = getColumns({
mutateRulesStates,
selectedRules,
setSelectedRules,
items,
@ -145,8 +131,6 @@ export const RulesTable = ({
isAllRulesSelectedThisPage,
isCurrentPageRulesASubset,
onRuleClick,
http,
startServices,
});
return (
@ -168,15 +152,12 @@ export const RulesTable = ({
};
const getColumns = ({
mutateRulesStates,
selectedRules,
setSelectedRules,
items,
isAllRulesSelectedThisPage,
isCurrentPageRulesASubset,
onRuleClick,
http,
startServices,
}: GetColumnProps): Array<EuiTableFieldDataColumnType<CspBenchmarkRulesWithStates>> => [
{
field: 'action',
@ -281,52 +262,43 @@ const getColumns = ({
align: 'right',
width: '100px',
truncateText: true,
render: (_name, rule: CspBenchmarkRulesWithStates) => {
const rulesObjectRequest = {
benchmark_id: rule?.metadata.benchmark.id,
benchmark_version: rule?.metadata.benchmark.version,
/* Rule number always exists from 8.7 */
rule_number: rule?.metadata.benchmark.rule_number!,
rule_id: rule?.metadata.id,
};
const isRuleMuted = rule?.state === 'muted';
const nextRuleState = isRuleMuted ? 'unmute' : 'mute';
const changeCspRuleStateFn = async () => {
if (rule?.metadata.benchmark.rule_number) {
// Calling this function this way to make sure it didn't get called on every single row render, its only being called when user click on the switch button
const detectionRulesForSelectedRule = (
await fetchDetectionRulesByTags(
getFindingsDetectionRuleSearchTags(rule.metadata),
{ match: 'all' },
http
)
).total;
mutateRulesStates({
newState: nextRuleState,
ruleIds: [rulesObjectRequest],
});
showChangeBenchmarkRuleStatesSuccessToast(startServices, isRuleMuted, {
numberOfRules: 1,
numberOfDetectionRules: detectionRulesForSelectedRule || 0,
});
}
};
return (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiSwitch
className="eui-textTruncate"
checked={!isRuleMuted}
onChange={changeCspRuleStateFn}
data-test-subj={RULES_ROWS_ENABLE_SWITCH_BUTTON}
label=""
compressed={true}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
},
render: (_name, rule: CspBenchmarkRulesWithStates) => <RuleStateSwitch rule={rule} />,
},
];
const RuleStateSwitch = ({ rule }: { rule: CspBenchmarkRulesWithStates }) => {
const isRuleMuted = rule?.state === 'muted';
const nextRuleState = isRuleMuted ? 'unmute' : 'mute';
const { mutate: mutateRulesStates } = useChangeCspRuleState();
const rulesObjectRequest = {
benchmark_id: rule?.metadata.benchmark.id,
benchmark_version: rule?.metadata.benchmark.version,
/* Rule number always exists from 8.7 */
rule_number: rule?.metadata.benchmark.rule_number!,
rule_id: rule?.metadata.id,
};
const changeCspRuleStateFn = async () => {
if (rule?.metadata.benchmark.rule_number) {
mutateRulesStates({
newState: nextRuleState,
ruleIds: [rulesObjectRequest],
});
}
};
return (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiSwitch
className="eui-textTruncate"
checked={!isRuleMuted}
onChange={changeCspRuleStateFn}
data-test-subj={RULES_ROWS_ENABLE_SWITCH_BUTTON}
label=""
compressed={true}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -23,16 +23,12 @@ import useDebounce from 'react-use/lib/useDebounce';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { useKibana } from '../../common/hooks/use_kibana';
import { getFindingsDetectionRuleSearchTagsFromArrayOfRules } from '../../../common/utils/detection_rules';
import {
RuleStateAttributesWithoutStates,
useChangeCspRuleState,
} from './use_change_csp_rule_state';
import { CspBenchmarkRulesWithStates } from './rules_container';
import { MultiSelectFilter } from '../../common/component/multi_select_filter';
import { showChangeBenchmarkRuleStatesSuccessToast } from '../../components/take_action';
import { useFetchDetectionRulesByTags } from '../../common/api/use_fetch_detection_rules_by_tags';
export const RULES_BULK_ACTION_BUTTON = 'bulk-action-button';
export const RULES_BULK_ACTION_OPTION_ENABLE = 'bulk-action-option-enable';
@ -245,15 +241,8 @@ const CurrentPageOfTotal = ({
};
const { mutate: mutateRulesStates } = useChangeCspRuleState();
const { data: detectionRulesForSelectedRules } = useFetchDetectionRulesByTags(
getFindingsDetectionRuleSearchTagsFromArrayOfRules(selectedRules.map((rule) => rule.metadata)),
{ match: 'any' }
);
const { notifications, analytics, i18n: i18nStart, theme } = useKibana().services;
const startServices = { notifications, analytics, i18n: i18nStart, theme };
const changeRulesState = async (state: 'mute' | 'unmute') => {
const changeCspRuleState = (state: 'mute' | 'unmute') => {
const bulkSelectedRules: RuleStateAttributesWithoutStates[] = selectedRules.map(
(e: CspBenchmarkRulesWithStates) => ({
benchmark_id: e?.metadata.benchmark.id,
@ -269,19 +258,15 @@ const CurrentPageOfTotal = ({
ruleIds: bulkSelectedRules,
});
setIsPopoverOpen(false);
showChangeBenchmarkRuleStatesSuccessToast(startServices, state !== 'mute', {
numberOfRules: bulkSelectedRules.length,
numberOfDetectionRules: detectionRulesForSelectedRules?.total || 0,
});
}
};
const changeCspRuleStateMute = async () => {
await changeRulesState('mute');
setSelectedRules([]);
};
const changeCspRuleStateUnmute = async () => {
await changeRulesState('unmute');
setSelectedRules([]);
const changeCspRuleStateMute = () => {
changeCspRuleState('mute');
};
const changeCspRuleStateUnmute = () => {
changeCspRuleState('unmute');
};
const areAllSelectedRulesMuted = selectedRules.every((rule) => rule?.state === 'muted');

View file

@ -1,100 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useQueryClient, useMutation } from '@tanstack/react-query';
import { CSP_RULES_STATES_QUERY_KEY } from './use_csp_rules_state';
import { CSPM_STATS_QUERY_KEY, KSPM_STATS_QUERY_KEY } from '../../common/api';
import { BENCHMARK_INTEGRATION_QUERY_KEY_V2 } from '../benchmarks/use_csp_benchmark_integrations';
import {
CspBenchmarkRulesBulkActionRequestSchema,
RuleStateAttributes,
} from '../../../common/types/latest';
import { CSP_BENCHMARK_RULES_BULK_ACTION_ROUTE_PATH } from '../../../common/constants';
export type RuleStateAttributesWithoutStates = Omit<RuleStateAttributes, 'muted'>;
export interface RuleStateUpdateRequest {
newState: 'mute' | 'unmute';
ruleIds: RuleStateAttributesWithoutStates[];
}
export const useChangeCspRuleState = () => {
const { http } = useKibana().services;
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (ruleStateUpdateRequest: RuleStateUpdateRequest) => {
await http?.post<CspBenchmarkRulesBulkActionRequestSchema>(
CSP_BENCHMARK_RULES_BULK_ACTION_ROUTE_PATH,
{
version: '1',
body: JSON.stringify({
action: ruleStateUpdateRequest.newState,
rules: ruleStateUpdateRequest.ruleIds,
}),
}
);
},
onMutate: async (ruleStateUpdateRequest: RuleStateUpdateRequest) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(CSP_RULES_STATES_QUERY_KEY);
// Snapshot the previous rules
const previousCspRules = queryClient.getQueryData(CSP_RULES_STATES_QUERY_KEY);
// Optimistically update to the rules that have state changes
queryClient.setQueryData(
CSP_RULES_STATES_QUERY_KEY,
(currentRuleStates: Record<string, RuleStateAttributes> | undefined) => {
if (!currentRuleStates) {
return currentRuleStates;
}
return createRulesWithUpdatedState(ruleStateUpdateRequest, currentRuleStates);
}
);
// Return a context object with the previous value
return { previousCspRules };
},
onSettled: () => {
queryClient.invalidateQueries(BENCHMARK_INTEGRATION_QUERY_KEY_V2);
queryClient.invalidateQueries(CSPM_STATS_QUERY_KEY);
queryClient.invalidateQueries(KSPM_STATS_QUERY_KEY);
queryClient.invalidateQueries(CSP_RULES_STATES_QUERY_KEY);
},
onError: (err, variables, context) => {
if (context?.previousCspRules) {
queryClient.setQueryData(CSP_RULES_STATES_QUERY_KEY, context.previousCspRules);
}
},
});
};
export function createRulesWithUpdatedState(
ruleStateUpdateRequest: RuleStateUpdateRequest,
currentRuleStates: Record<string, RuleStateAttributes>
) {
const updateRuleStates: Record<string, RuleStateAttributes> = {};
ruleStateUpdateRequest.ruleIds.forEach((ruleId) => {
const matchingRuleKey = Object.keys(currentRuleStates).find(
(key) => currentRuleStates[key].rule_id === ruleId.rule_id
);
if (matchingRuleKey) {
const updatedRule = {
...currentRuleStates[matchingRuleKey],
muted: ruleStateUpdateRequest.newState === 'mute',
};
updateRuleStates[matchingRuleKey] = updatedRule;
}
});
return {
...currentRuleStates,
...updateRuleStates,
};
}

View file

@ -0,0 +1,204 @@
/*
* 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 { useQueryClient, useMutation } from '@tanstack/react-query';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n as kbnI18n } from '@kbn/i18n';
import { CSP_RULES_STATES_QUERY_KEY } from './use_csp_rules_state';
import { CSPM_STATS_QUERY_KEY, KSPM_STATS_QUERY_KEY } from '../../common/api';
import { BENCHMARK_INTEGRATION_QUERY_KEY_V2 } from '../benchmarks/use_csp_benchmark_integrations';
import {
CspBenchmarkRulesBulkActionResponse,
RuleStateAttributes,
} from '../../../common/types/latest';
import { CSP_BENCHMARK_RULES_BULK_ACTION_ROUTE_PATH } from '../../../common/constants';
import { CloudSecurityPostureStartServices } from '../../types';
import { useKibana } from '../../common/hooks/use_kibana';
export type RuleStateAttributesWithoutStates = Omit<RuleStateAttributes, 'muted'>;
export interface RuleStateUpdateRequest {
newState: 'mute' | 'unmute';
ruleIds: RuleStateAttributesWithoutStates[];
}
export const showChangeBenchmarkRulesStatesErrorToast = (
cloudSecurityStartServices: CloudSecurityPostureStartServices,
error: Error
) => {
return cloudSecurityStartServices.notifications.toasts.addDanger({
title: kbnI18n.translate('xpack.csp.rules.changeRuleStateErrorTitle', {
defaultMessage: 'Unable to update rule',
}),
text: kbnI18n.translate('xpack.csp.rules.changeRuleStateErrorText', {
defaultMessage: 'An error occurred while updating the rule: {errorMessage}.',
values: { errorMessage: error.message },
}),
'data-test-subj': 'csp:toast-error',
});
};
const showChangeBenchmarkRuleStatesSuccessToast = (
cloudSecurityStartServices: CloudSecurityPostureStartServices,
data: {
newState: RuleStateUpdateRequest['newState'];
numberOfRules: number;
numberOfDetectionRules: number;
}
) => {
const { notifications, analytics, i18n, theme } = cloudSecurityStartServices;
const startServices = { analytics, i18n, theme };
return notifications.toasts.addSuccess({
toastLifeTimeMs: 10000,
color: 'success',
iconType: '',
'data-test-subj': 'csp:toast-success-rule-state-change',
title: toMountPoint(
<EuiText size="m">
<strong data-test-subj={`csp:toast-success-rule-title`}>
{data.newState === 'unmute' ? (
<FormattedMessage
id="xpack.csp.flyout.ruleEnabledToastTitle"
defaultMessage="Rule Enabled"
/>
) : (
<FormattedMessage
id="xpack.csp.flyout.ruleDisabledToastTitle"
defaultMessage="Rule Disabled"
/>
)}
</strong>
</EuiText>,
startServices
),
text: toMountPoint(
<div>
{data.newState === 'unmute' ? (
<FormattedMessage
id="xpack.csp.flyout.ruleEnabledToastRulesCount"
defaultMessage="Successfully enabled {ruleCount, plural, one {# rule} other {# rules}} "
values={{
ruleCount: data.numberOfRules,
}}
/>
) : (
<>
<FormattedMessage
id="xpack.csp.flyout.ruleDisabledToastRulesCount"
defaultMessage="Successfully disabled {ruleCount, plural, one {# rule} other {# rules}} "
values={{
ruleCount: data.numberOfRules,
}}
/>
{data.newState === 'mute' && data.numberOfDetectionRules > 0 && (
<strong>
<FormattedMessage
id="xpack.csp.flyout.ruleDisabledToastDetectionRulesCount"
defaultMessage=" and {detectionRuleCount, plural, one {# detection rule} other {# detection rules}}"
values={{
detectionRuleCount: data.numberOfDetectionRules,
}}
/>
</strong>
)}
</>
)}
</div>,
startServices
),
});
};
export const useChangeCspRuleState = () => {
const { http } = useKibana().services;
const { notifications, analytics, i18n: i18nStart, theme } = useKibana().services;
const startServices = { notifications, analytics, i18n: i18nStart, theme };
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (ruleStateUpdateRequest: RuleStateUpdateRequest) => {
return await http?.post<CspBenchmarkRulesBulkActionResponse>(
CSP_BENCHMARK_RULES_BULK_ACTION_ROUTE_PATH,
{
version: '1',
body: JSON.stringify({
action: ruleStateUpdateRequest.newState,
rules: ruleStateUpdateRequest.ruleIds,
}),
}
);
},
onMutate: async (ruleStateUpdateRequest: RuleStateUpdateRequest) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(CSP_RULES_STATES_QUERY_KEY);
// Snapshot the previous rules
const previousCspRules = queryClient.getQueryData(CSP_RULES_STATES_QUERY_KEY);
// Optimistically update to the rules that have state changes
queryClient.setQueryData(
CSP_RULES_STATES_QUERY_KEY,
(currentRuleStates: Record<string, RuleStateAttributes> | undefined) => {
if (!currentRuleStates) {
return currentRuleStates;
}
return createRulesWithUpdatedState(ruleStateUpdateRequest, currentRuleStates);
}
);
// Return a context object with the previous value
return { previousCspRules };
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries(BENCHMARK_INTEGRATION_QUERY_KEY_V2);
queryClient.invalidateQueries(CSPM_STATS_QUERY_KEY);
queryClient.invalidateQueries(KSPM_STATS_QUERY_KEY);
queryClient.invalidateQueries(CSP_RULES_STATES_QUERY_KEY);
showChangeBenchmarkRuleStatesSuccessToast(startServices, {
newState: variables?.newState,
numberOfRules: Object.keys(data?.updated_benchmark_rules || {})?.length || 0,
numberOfDetectionRules: data?.disabled_detection_rules?.length || 0,
});
},
onError: (error: Error, _, context) => {
if (context?.previousCspRules) {
queryClient.setQueryData(CSP_RULES_STATES_QUERY_KEY, context.previousCspRules);
}
showChangeBenchmarkRulesStatesErrorToast(startServices, error);
},
});
};
export function createRulesWithUpdatedState(
ruleStateUpdateRequest: RuleStateUpdateRequest,
currentRuleStates: Record<string, RuleStateAttributes>
) {
const updateRuleStates: Record<string, RuleStateAttributes> = {};
ruleStateUpdateRequest.ruleIds.forEach((ruleId) => {
const matchingRuleKey = Object.keys(currentRuleStates).find(
(key) => currentRuleStates[key].rule_id === ruleId.rule_id
);
if (matchingRuleKey) {
const updatedRule = {
...currentRuleStates[matchingRuleKey],
muted: ruleStateUpdateRequest.newState === 'mute',
};
updateRuleStates[matchingRuleKey] = updatedRule;
}
});
return {
...currentRuleStates,
...updateRuleStates,
};
}

View file

@ -59,7 +59,7 @@ export const getDetectionRules = async (
filter: convertRuleTagsToMatchAllKQL(ruleTags),
searchFields: ['tags'],
page: 1,
perPage: 1,
perPage: 100, // Disable up to 100 detection rules per benchmark rule at a time
},
});
})