mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
8ba84ecf00
commit
259518214a
8 changed files with 297 additions and 291 deletions
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue