[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,41 +178,41 @@ export const deleteRules = async ({ ids }: DeleteRulesProps): Promise<Array<Rule
|
||||||
/**
|
/**
|
||||||
* Duplicates provided Rules
|
* Duplicates provided Rules
|
||||||
*
|
*
|
||||||
* @param rule to duplicate
|
* @param rules to duplicate
|
||||||
*/
|
*/
|
||||||
export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Rule[]> => {
|
export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Rule[]> => {
|
||||||
const requests = rules.map(rule =>
|
const response = await fetch(
|
||||||
fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, {
|
`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_create`,
|
||||||
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
'kbn-xsrf': 'true',
|
'kbn-xsrf': 'true',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(
|
||||||
...rule,
|
rules.map(rule => ({
|
||||||
name: `${rule.name} [${i18n.DUPLICATE}]`,
|
...rule,
|
||||||
created_at: undefined,
|
name: `${rule.name} [${i18n.DUPLICATE}]`,
|
||||||
created_by: undefined,
|
created_at: undefined,
|
||||||
id: undefined,
|
created_by: undefined,
|
||||||
rule_id: undefined,
|
id: undefined,
|
||||||
updated_at: undefined,
|
rule_id: undefined,
|
||||||
updated_by: undefined,
|
updated_at: undefined,
|
||||||
enabled: rule.enabled,
|
updated_by: undefined,
|
||||||
immutable: false,
|
enabled: rule.enabled,
|
||||||
last_success_at: undefined,
|
immutable: undefined,
|
||||||
last_success_message: undefined,
|
last_success_at: undefined,
|
||||||
status: undefined,
|
last_success_message: undefined,
|
||||||
status_date: undefined,
|
status: undefined,
|
||||||
}),
|
status_date: undefined,
|
||||||
})
|
}))
|
||||||
|
),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const responses = await Promise.all(requests);
|
await throwIfNotOk(response);
|
||||||
await responses.map(response => throwIfNotOk(response));
|
return response.json();
|
||||||
return Promise.all(
|
|
||||||
responses.map<Promise<Rule>>(response => response.json())
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -146,7 +146,7 @@ export interface DeleteRulesProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DuplicateRulesProps {
|
export interface DuplicateRulesProps {
|
||||||
rules: Rules;
|
rules: Rule[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BasicFetchProps {
|
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`);
|
history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const duplicateRuleAction = async (
|
export const duplicateRulesAction = async (
|
||||||
rule: Rule,
|
rules: Rule[],
|
||||||
dispatch: React.Dispatch<Action>,
|
dispatch: React.Dispatch<Action>,
|
||||||
dispatchToaster: Dispatch<ActionToaster>
|
dispatchToaster: Dispatch<ActionToaster>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: true });
|
const ruleIds = rules.map(r => r.id);
|
||||||
const duplicatedRule = await duplicateRules({ rules: [rule] });
|
dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: true });
|
||||||
dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false });
|
const duplicatedRules = await duplicateRules({ rules });
|
||||||
dispatch({ type: 'updateRules', rules: duplicatedRule, appendRuleId: rule.id });
|
dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: false });
|
||||||
displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRule.length), dispatchToaster);
|
dispatch({
|
||||||
|
type: 'updateRules',
|
||||||
|
rules: duplicatedRules,
|
||||||
|
appendRuleId: rules[rules.length - 1].id,
|
||||||
|
});
|
||||||
|
displaySuccessToast(
|
||||||
|
i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRules.length),
|
||||||
|
dispatchToaster
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
displayErrorToast(i18n.DUPLICATE_RULE_ERROR, [e.message], dispatchToaster);
|
displayErrorToast(i18n.DUPLICATE_RULE_ERROR, [e.message], dispatchToaster);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,13 @@ import * as H from 'history';
|
||||||
import * as i18n from '../translations';
|
import * as i18n from '../translations';
|
||||||
import { TableData } from '../types';
|
import { TableData } from '../types';
|
||||||
import { Action } from './reducer';
|
import { Action } from './reducer';
|
||||||
import { deleteRulesAction, enableRulesAction, exportRulesAction } from './actions';
|
import {
|
||||||
|
deleteRulesAction,
|
||||||
|
duplicateRulesAction,
|
||||||
|
enableRulesAction,
|
||||||
|
exportRulesAction,
|
||||||
|
} from './actions';
|
||||||
import { ActionToaster } from '../../../../components/toasters';
|
import { ActionToaster } from '../../../../components/toasters';
|
||||||
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine';
|
|
||||||
|
|
||||||
export const getBatchItems = (
|
export const getBatchItems = (
|
||||||
selectedState: TableData[],
|
selectedState: TableData[],
|
||||||
|
@ -25,7 +29,6 @@ export const getBatchItems = (
|
||||||
const containsDisabled = selectedState.some(v => !v.activate);
|
const containsDisabled = selectedState.some(v => !v.activate);
|
||||||
const containsLoading = selectedState.some(v => v.isLoading);
|
const containsLoading = selectedState.some(v => v.isLoading);
|
||||||
const containsImmutable = selectedState.some(v => v.immutable);
|
const containsImmutable = selectedState.some(v => v.immutable);
|
||||||
const containsMultipleRules = Array.from(new Set(selectedState.map(v => v.rule_id))).length > 1;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<EuiContextMenuItem
|
<EuiContextMenuItem
|
||||||
|
@ -67,23 +70,25 @@ export const getBatchItems = (
|
||||||
{i18n.BATCH_ACTION_EXPORT_SELECTED}
|
{i18n.BATCH_ACTION_EXPORT_SELECTED}
|
||||||
</EuiContextMenuItem>,
|
</EuiContextMenuItem>,
|
||||||
<EuiContextMenuItem
|
<EuiContextMenuItem
|
||||||
key={i18n.BATCH_ACTION_EDIT_INDEX_PATTERNS}
|
key={i18n.BATCH_ACTION_DUPLICATE_SELECTED}
|
||||||
icon="indexEdit"
|
icon="copy"
|
||||||
disabled={
|
disabled={containsLoading || selectedState.length === 0}
|
||||||
containsImmutable || containsLoading || containsMultipleRules || selectedState.length === 0
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
closePopover();
|
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>,
|
||||||
<EuiContextMenuItem
|
<EuiContextMenuItem
|
||||||
key={i18n.BATCH_ACTION_DELETE_SELECTED}
|
key={i18n.BATCH_ACTION_DELETE_SELECTED}
|
||||||
icon="trash"
|
icon="trash"
|
||||||
title={containsImmutable ? i18n.BATCH_ACTION_DELETE_SELECTED_IMMUTABLE : undefined}
|
title={containsImmutable ? i18n.BATCH_ACTION_DELETE_SELECTED_IMMUTABLE : undefined}
|
||||||
disabled={containsImmutable || containsLoading || selectedState.length === 0}
|
disabled={containsLoading || selectedState.length === 0}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
closePopover();
|
closePopover();
|
||||||
await deleteRulesAction(
|
await deleteRulesAction(
|
||||||
|
|
|
@ -18,7 +18,7 @@ import React, { Dispatch } from 'react';
|
||||||
import { getEmptyTagValue } from '../../../../components/empty_value';
|
import { getEmptyTagValue } from '../../../../components/empty_value';
|
||||||
import {
|
import {
|
||||||
deleteRulesAction,
|
deleteRulesAction,
|
||||||
duplicateRuleAction,
|
duplicateRulesAction,
|
||||||
editRuleAction,
|
editRuleAction,
|
||||||
exportRulesAction,
|
exportRulesAction,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
@ -48,7 +48,7 @@ const getActions = (
|
||||||
icon: 'copy',
|
icon: 'copy',
|
||||||
name: i18n.DUPLICATE_RULE,
|
name: i18n.DUPLICATE_RULE,
|
||||||
onClick: (rowItem: TableData) =>
|
onClick: (rowItem: TableData) =>
|
||||||
duplicateRuleAction(rowItem.sourceRule, dispatch, dispatchToaster),
|
duplicateRulesAction([rowItem.sourceRule], dispatch, dispatchToaster),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: i18n.EXPORT_RULE,
|
description: i18n.EXPORT_RULE,
|
||||||
|
@ -62,7 +62,6 @@ const getActions = (
|
||||||
icon: 'trash',
|
icon: 'trash',
|
||||||
name: i18n.DELETE_RULE,
|
name: i18n.DELETE_RULE,
|
||||||
onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, dispatchToaster),
|
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 { Rule } from '../../../../../containers/detection_engine/rules';
|
||||||
import * as i18n from './translations';
|
import * as i18n from './translations';
|
||||||
import * as i18nActions from '../../../rules/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 { displaySuccessToast, useStateToaster } from '../../../../../components/toasters';
|
||||||
import { RuleDownloader } from '../rule_downloader';
|
import { RuleDownloader } from '../rule_downloader';
|
||||||
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../components/link_to/redirect_to_detection_engine';
|
import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../components/link_to/redirect_to_detection_engine';
|
||||||
|
@ -54,7 +54,7 @@ const RuleActionsOverflowComponent = ({
|
||||||
disabled={userHasNoPermissions}
|
disabled={userHasNoPermissions}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsPopoverOpen(false);
|
setIsPopoverOpen(false);
|
||||||
await duplicateRuleAction(rule, noop, dispatchToaster);
|
await duplicateRulesAction([rule], noop, dispatchToaster);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{i18nActions.DUPLICATE_RULE}
|
{i18nActions.DUPLICATE_RULE}
|
||||||
|
@ -73,7 +73,7 @@ const RuleActionsOverflowComponent = ({
|
||||||
<EuiContextMenuItem
|
<EuiContextMenuItem
|
||||||
key={i18nActions.DELETE_RULE}
|
key={i18nActions.DELETE_RULE}
|
||||||
icon="trash"
|
icon="trash"
|
||||||
disabled={userHasNoPermissions || rule.immutable}
|
disabled={userHasNoPermissions}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsPopoverOpen(false);
|
setIsPopoverOpen(false);
|
||||||
await deleteRulesAction([rule.id], noop, dispatchToaster, onRuleDeletedCallback);
|
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(
|
export const BATCH_ACTION_DUPLICATE_SELECTED = i18n.translate(
|
||||||
'xpack.siem.detectionEngine.rules.allRules.batchActions.editIndexPatternsTitle',
|
'xpack.siem.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle',
|
||||||
{
|
{
|
||||||
defaultMessage: 'Edit selected index patterns…',
|
defaultMessage: 'Duplicate selected…',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue