Skip to content

Commit

Permalink
[SIEM] [Detection Engine] Fixes duplicate rule action (#55252) (#55261)
Browse files Browse the repository at this point in the history
## 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 #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)~
  • Loading branch information
spong authored and XavierM committed Jan 18, 2020
1 parent 7e7b18d commit 50ed783
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 53 deletions.
Expand Up @@ -178,41 +178,41 @@ 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({
...rule,
name: `${rule.name} [${i18n.DUPLICATE}]`,
created_at: undefined,
created_by: undefined,
id: undefined,
rule_id: undefined,
updated_at: undefined,
updated_by: undefined,
enabled: rule.enabled,
immutable: false,
last_success_at: undefined,
last_success_message: undefined,
status: undefined,
status_date: undefined,
}),
})
body: JSON.stringify(
rules.map(rule => ({
...rule,
name: `${rule.name} [${i18n.DUPLICATE}]`,
created_at: undefined,
created_by: undefined,
id: undefined,
rule_id: undefined,
updated_at: undefined,
updated_by: undefined,
enabled: rule.enabled,
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();
};

/**
Expand Down
Expand Up @@ -146,7 +146,7 @@ export interface DeleteRulesProps {
}

export interface DuplicateRulesProps {
rules: Rules;
rules: Rule[];
}

export interface BasicFetchProps {
Expand Down
Expand Up @@ -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);
}
Expand Down
Expand Up @@ -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[],
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Expand Up @@ -18,7 +18,7 @@ import React, { Dispatch } from 'react';
import { getEmptyTagValue } from '../../../../components/empty_value';
import {
deleteRulesAction,
duplicateRuleAction,
duplicateRulesAction,
editRuleAction,
exportRulesAction,
} from './actions';
Expand Down Expand Up @@ -50,7 +50,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,
Expand All @@ -66,7 +66,6 @@ const getActions = (
icon: 'trash',
name: i18n.DELETE_RULE,
onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, dispatchToaster),
enabled: (rowItem: TableData) => !rowItem.immutable,
},
];

Expand Down
Expand Up @@ -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';
Expand Down Expand Up @@ -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}
Expand All @@ -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);
Expand Down
Expand Up @@ -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…',
}
);

Expand Down

0 comments on commit 50ed783

Please sign in to comment.