[SIEM] [Detection Engine] Fixes duplicate rule action (#55252)
## Summary This PR fixes the duplication of rules. The DE backend was updated to not allow `immutable` when creating a rule, so this broke the `Duplicate Rule` action as we were creating a new rule with `immutable: false`. This PR also switches rule duplication over to use the bulk `create` API introduced in https://github.com/elastic/kibana/pull/53543, so now we can duplicate multiple rules. And lastly, this PR removes the limitation of not being able to delete immutable rules. So long as you have the appropriate `write` permissions the delete action is now always available. ![duplicate_batch](https://user-images.githubusercontent.com/2946766/72652638-cee69a00-3944-11ea-9e15-cce3f2b8cefe.gif) ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [ ] ~This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~ - [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/master/packages/kbn-i18n/README.md) - [ ] ~[Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~ - [ ] ~[Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios~ - [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~ ### For maintainers - [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ - [ ] ~This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~
This commit is contained in:
parent
6760c3394d
commit
dfb3578793
|
@ -178,18 +178,20 @@ export const deleteRules = async ({ ids }: DeleteRulesProps): Promise<Array<Rule
|
|||
/**
|
||||
* Duplicates provided Rules
|
||||
*
|
||||
* @param rule to duplicate
|
||||
* @param rules to duplicate
|
||||
*/
|
||||
export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Rule[]> => {
|
||||
const requests = rules.map(rule =>
|
||||
fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, {
|
||||
const response = await fetch(
|
||||
`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_create`,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'kbn-xsrf': 'true',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
body: JSON.stringify(
|
||||
rules.map(rule => ({
|
||||
...rule,
|
||||
name: `${rule.name} [${i18n.DUPLICATE}]`,
|
||||
created_at: undefined,
|
||||
|
@ -199,20 +201,18 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Ru
|
|||
updated_at: undefined,
|
||||
updated_by: undefined,
|
||||
enabled: rule.enabled,
|
||||
immutable: false,
|
||||
immutable: undefined,
|
||||
last_success_at: undefined,
|
||||
last_success_message: undefined,
|
||||
status: undefined,
|
||||
status_date: undefined,
|
||||
}),
|
||||
})
|
||||
}))
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
await responses.map(response => throwIfNotOk(response));
|
||||
return Promise.all(
|
||||
responses.map<Promise<Rule>>(response => response.json())
|
||||
);
|
||||
await throwIfNotOk(response);
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -146,7 +146,7 @@ export interface DeleteRulesProps {
|
|||
}
|
||||
|
||||
export interface DuplicateRulesProps {
|
||||
rules: Rules;
|
||||
rules: Rule[];
|
||||
}
|
||||
|
||||
export interface BasicFetchProps {
|
||||
|
|
|
@ -29,17 +29,25 @@ export const editRuleAction = (rule: Rule, history: H.History) => {
|
|||
history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`);
|
||||
};
|
||||
|
||||
export const duplicateRuleAction = async (
|
||||
rule: Rule,
|
||||
export const duplicateRulesAction = async (
|
||||
rules: Rule[],
|
||||
dispatch: React.Dispatch<Action>,
|
||||
dispatchToaster: Dispatch<ActionToaster>
|
||||
) => {
|
||||
try {
|
||||
dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: true });
|
||||
const duplicatedRule = await duplicateRules({ rules: [rule] });
|
||||
dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false });
|
||||
dispatch({ type: 'updateRules', rules: duplicatedRule, appendRuleId: rule.id });
|
||||
displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRule.length), dispatchToaster);
|
||||
const ruleIds = rules.map(r => r.id);
|
||||
dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: true });
|
||||
const duplicatedRules = await duplicateRules({ rules });
|
||||
dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: false });
|
||||
dispatch({
|
||||
type: 'updateRules',
|
||||
rules: duplicatedRules,
|
||||
appendRuleId: rules[rules.length - 1].id,
|
||||
});
|
||||
displaySuccessToast(
|
||||
i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRules.length),
|
||||
dispatchToaster
|
||||
);
|
||||
} catch (e) {
|
||||
displayErrorToast(i18n.DUPLICATE_RULE_ERROR, [e.message], dispatchToaster);
|
||||
}
|
||||
|
|
|
@ -10,9 +10,13 @@ import * as H from 'history';
|
|||
import * as i18n from '../translations';
|
||||
import { TableData } from '../types';
|
||||
import { Action } from './reducer';
|
||||
import { deleteRulesAction, enableRulesAction, exportRulesAction } from './actions';
|
||||
import {
|
||||
deleteRulesAction,
|
||||
duplicateRulesAction,
|
||||
enableRulesAction,
|
||||
exportRulesAction,
|
||||
} from './actions';
|
||||
import { ActionToaster } from '../../../../components/toasters';
|
||||
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
|
||||
|
||||
export const getBatchItems = (
|
||||
selectedState: TableData[],
|
||||
|
@ -25,7 +29,6 @@ export const getBatchItems = (
|
|||
const containsDisabled = selectedState.some(v => !v.activate);
|
||||
const containsLoading = selectedState.some(v => v.isLoading);
|
||||
const containsImmutable = selectedState.some(v => v.immutable);
|
||||
const containsMultipleRules = Array.from(new Set(selectedState.map(v => v.rule_id))).length > 1;
|
||||
|
||||
return [
|
||||
<EuiContextMenuItem
|
||||
|
@ -67,23 +70,25 @@ export const getBatchItems = (
|
|||
{i18n.BATCH_ACTION_EXPORT_SELECTED}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key={i18n.BATCH_ACTION_EDIT_INDEX_PATTERNS}
|
||||
icon="indexEdit"
|
||||
disabled={
|
||||
containsImmutable || containsLoading || containsMultipleRules || selectedState.length === 0
|
||||
}
|
||||
key={i18n.BATCH_ACTION_DUPLICATE_SELECTED}
|
||||
icon="copy"
|
||||
disabled={containsLoading || selectedState.length === 0}
|
||||
onClick={async () => {
|
||||
closePopover();
|
||||
history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${selectedState[0].id}/edit`);
|
||||
await duplicateRulesAction(
|
||||
selectedState.map(s => s.sourceRule),
|
||||
dispatch,
|
||||
dispatchToaster
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n.BATCH_ACTION_EDIT_INDEX_PATTERNS}
|
||||
{i18n.BATCH_ACTION_DUPLICATE_SELECTED}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key={i18n.BATCH_ACTION_DELETE_SELECTED}
|
||||
icon="trash"
|
||||
title={containsImmutable ? i18n.BATCH_ACTION_DELETE_SELECTED_IMMUTABLE : undefined}
|
||||
disabled={containsImmutable || containsLoading || selectedState.length === 0}
|
||||
disabled={containsLoading || selectedState.length === 0}
|
||||
onClick={async () => {
|
||||
closePopover();
|
||||
await deleteRulesAction(
|
||||
|
|
|
@ -18,7 +18,7 @@ import React, { Dispatch } from 'react';
|
|||
import { getEmptyTagValue } from '../../../../components/empty_value';
|
||||
import {
|
||||
deleteRulesAction,
|
||||
duplicateRuleAction,
|
||||
duplicateRulesAction,
|
||||
editRuleAction,
|
||||
exportRulesAction,
|
||||
} from './actions';
|
||||
|
@ -48,7 +48,7 @@ const getActions = (
|
|||
icon: 'copy',
|
||||
name: i18n.DUPLICATE_RULE,
|
||||
onClick: (rowItem: TableData) =>
|
||||
duplicateRuleAction(rowItem.sourceRule, dispatch, dispatchToaster),
|
||||
duplicateRulesAction([rowItem.sourceRule], dispatch, dispatchToaster),
|
||||
},
|
||||
{
|
||||
description: i18n.EXPORT_RULE,
|
||||
|
@ -62,7 +62,6 @@ const getActions = (
|
|||
icon: 'trash',
|
||||
name: i18n.DELETE_RULE,
|
||||
onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, dispatchToaster),
|
||||
enabled: (rowItem: TableData) => !rowItem.immutable,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import { useHistory } from 'react-router-dom';
|
|||
import { Rule } from '../../../../../containers/detection_engine/rules';
|
||||
import * as i18n from './translations';
|
||||
import * as i18nActions from '../../../rules/translations';
|
||||
import { deleteRulesAction, duplicateRuleAction } from '../../all/actions';
|
||||
import { deleteRulesAction, duplicateRulesAction } from '../../all/actions';
|
||||
import { displaySuccessToast, useStateToaster } from '../../../../../components/toasters';
|
||||
import { RuleDownloader } from '../rule_downloader';
|
||||
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../components/link_to/redirect_to_detection_engine';
|
||||
|
@ -54,7 +54,7 @@ const RuleActionsOverflowComponent = ({
|
|||
disabled={userHasNoPermissions}
|
||||
onClick={async () => {
|
||||
setIsPopoverOpen(false);
|
||||
await duplicateRuleAction(rule, noop, dispatchToaster);
|
||||
await duplicateRulesAction([rule], noop, dispatchToaster);
|
||||
}}
|
||||
>
|
||||
{i18nActions.DUPLICATE_RULE}
|
||||
|
@ -73,7 +73,7 @@ const RuleActionsOverflowComponent = ({
|
|||
<EuiContextMenuItem
|
||||
key={i18nActions.DELETE_RULE}
|
||||
icon="trash"
|
||||
disabled={userHasNoPermissions || rule.immutable}
|
||||
disabled={userHasNoPermissions}
|
||||
onClick={async () => {
|
||||
setIsPopoverOpen(false);
|
||||
await deleteRulesAction([rule.id], noop, dispatchToaster, onRuleDeletedCallback);
|
||||
|
|
|
@ -75,10 +75,10 @@ export const BATCH_ACTION_EXPORT_SELECTED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const BATCH_ACTION_EDIT_INDEX_PATTERNS = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.batchActions.editIndexPatternsTitle',
|
||||
export const BATCH_ACTION_DUPLICATE_SELECTED = i18n.translate(
|
||||
'xpack.siem.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle',
|
||||
{
|
||||
defaultMessage: 'Edit selected index patterns…',
|
||||
defaultMessage: 'Duplicate selected…',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in a new issue