mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Rule duplication with/without exceptions (#144782)
## Rule duplication with/without exceptions Majority of work done by @yctercero in this [branch](https://github.com/yctercero/kibana/tree/dupe) Some integration tests are left, but PR is ready for review. 2 flow when you duplicate rule: ### Without exceptions Don't duplicate any exceptions ### With exceptions Shared exceptions should duplicate reference Rule default exceptions are not duplicated by reference, but create a copy of exceptions. So if you remove it from duplicate rules, the original rule is not changed. https://user-images.githubusercontent.com/7609147/200863319-4cb56749-42dd-42d8-8896-f45782c21838.mov # TODO; [] integrations tests [] cypress tests Co-authored-by: Yara Tercero <yara.tercero@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
2355535972
commit
4868e2118d
26 changed files with 818 additions and 146 deletions
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import uuid from 'uuid';
|
||||
import type {
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListItemSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { SavedObjectType, getSavedObjectType } from '@kbn/securitysolution-list-utils';
|
||||
|
||||
import { ExceptionListSoSchema } from '../../schemas/saved_objects';
|
||||
|
||||
import { transformSavedObjectToExceptionListItem } from './utils';
|
||||
|
||||
interface BulkCreateExceptionListItemsOptions {
|
||||
items: CreateExceptionListItemSchema[];
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
user: string;
|
||||
tieBreaker?: string;
|
||||
}
|
||||
|
||||
export const bulkCreateExceptionListItems = async ({
|
||||
items,
|
||||
savedObjectsClient,
|
||||
tieBreaker,
|
||||
user,
|
||||
}: BulkCreateExceptionListItemsOptions): Promise<ExceptionListItemSchema[]> => {
|
||||
const formattedItems = items.map((item) => {
|
||||
const savedObjectType = getSavedObjectType({ namespaceType: item.namespace_type ?? 'single' });
|
||||
const dateNow = new Date().toISOString();
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
comments: [],
|
||||
created_at: dateNow,
|
||||
created_by: user,
|
||||
description: item.description,
|
||||
entries: item.entries,
|
||||
immutable: false,
|
||||
item_id: item.item_id,
|
||||
list_id: item.list_id,
|
||||
list_type: 'item',
|
||||
meta: item.meta,
|
||||
name: item.name,
|
||||
os_types: item.os_types,
|
||||
tags: item.tags,
|
||||
tie_breaker_id: tieBreaker ?? uuid.v4(),
|
||||
type: item.type,
|
||||
updated_by: user,
|
||||
version: undefined,
|
||||
},
|
||||
type: savedObjectType,
|
||||
} as { attributes: ExceptionListSoSchema; type: SavedObjectType };
|
||||
});
|
||||
|
||||
const { saved_objects: savedObjects } =
|
||||
await savedObjectsClient.bulkCreate<ExceptionListSoSchema>(formattedItems);
|
||||
|
||||
const result = savedObjects.map<ExceptionListItemSchema>((so) =>
|
||||
transformSavedObjectToExceptionListItem({ savedObject: so })
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import uuid from 'uuid';
|
||||
import {
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListSchema,
|
||||
ExceptionListTypeEnum,
|
||||
FoundExceptionListItemSchema,
|
||||
ListId,
|
||||
NamespaceType,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { findExceptionListsItemPointInTimeFinder } from './find_exception_list_items_point_in_time_finder';
|
||||
import { bulkCreateExceptionListItems } from './bulk_create_exception_list_items';
|
||||
import { getExceptionList } from './get_exception_list';
|
||||
import { createExceptionList } from './create_exception_list';
|
||||
|
||||
const LISTS_ABLE_TO_DUPLICATE = [
|
||||
ExceptionListTypeEnum.DETECTION.toString(),
|
||||
ExceptionListTypeEnum.RULE_DEFAULT.toString(),
|
||||
];
|
||||
|
||||
interface CreateExceptionListOptions {
|
||||
listId: ListId;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
namespaceType: NamespaceType;
|
||||
user: string;
|
||||
}
|
||||
|
||||
export const duplicateExceptionListAndItems = async ({
|
||||
listId,
|
||||
savedObjectsClient,
|
||||
namespaceType,
|
||||
user,
|
||||
}: CreateExceptionListOptions): Promise<ExceptionListSchema> => {
|
||||
// Generate a new static listId
|
||||
const newListId = uuid.v4();
|
||||
|
||||
// fetch list container
|
||||
const listToDuplicate = await getExceptionList({
|
||||
id: undefined,
|
||||
listId,
|
||||
namespaceType,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
if (listToDuplicate == null) {
|
||||
throw new Error(`Exception list to duplicat of list_id:${listId} not found.`);
|
||||
}
|
||||
|
||||
if (!LISTS_ABLE_TO_DUPLICATE.includes(listToDuplicate.type)) {
|
||||
throw new Error(`Exception list of type:${listToDuplicate.type} cannot be duplicated.`);
|
||||
}
|
||||
|
||||
const newlyCreatedList = await createExceptionList({
|
||||
description: listToDuplicate.description,
|
||||
immutable: listToDuplicate.immutable,
|
||||
listId: newListId,
|
||||
meta: listToDuplicate.meta,
|
||||
name: listToDuplicate.name,
|
||||
namespaceType: listToDuplicate.namespace_type,
|
||||
savedObjectsClient,
|
||||
tags: listToDuplicate.tags,
|
||||
type: listToDuplicate.type,
|
||||
user,
|
||||
version: 1,
|
||||
});
|
||||
|
||||
// fetch associated items
|
||||
let itemsToBeDuplicated: CreateExceptionListItemSchema[] = [];
|
||||
const executeFunctionOnStream = (response: FoundExceptionListItemSchema): void => {
|
||||
const transformedItems = response.data.map((item) => {
|
||||
// Generate a new static listId
|
||||
const newItemId = uuid.v4();
|
||||
|
||||
return {
|
||||
comments: [],
|
||||
description: item.description,
|
||||
entries: item.entries,
|
||||
item_id: newItemId,
|
||||
list_id: newlyCreatedList.list_id,
|
||||
meta: item.meta,
|
||||
name: item.name,
|
||||
namespace_type: item.namespace_type,
|
||||
os_types: item.os_types,
|
||||
tags: item.tags,
|
||||
type: item.type,
|
||||
};
|
||||
});
|
||||
itemsToBeDuplicated = [...itemsToBeDuplicated, ...transformedItems];
|
||||
};
|
||||
await findExceptionListsItemPointInTimeFinder({
|
||||
executeFunctionOnStream,
|
||||
filter: [],
|
||||
listId: [listId],
|
||||
maxSize: 10000,
|
||||
namespaceType: [namespaceType],
|
||||
perPage: undefined,
|
||||
savedObjectsClient,
|
||||
sortField: undefined,
|
||||
sortOrder: undefined,
|
||||
});
|
||||
|
||||
await bulkCreateExceptionListItems({
|
||||
items: itemsToBeDuplicated,
|
||||
savedObjectsClient,
|
||||
user,
|
||||
});
|
||||
|
||||
return newlyCreatedList;
|
||||
};
|
|
@ -39,6 +39,7 @@ import type {
|
|||
DeleteExceptionListItemByIdOptions,
|
||||
DeleteExceptionListItemOptions,
|
||||
DeleteExceptionListOptions,
|
||||
DuplicateExceptionListOptions,
|
||||
ExportExceptionListAndItemsOptions,
|
||||
FindEndpointListItemOptions,
|
||||
FindExceptionListItemOptions,
|
||||
|
@ -95,6 +96,7 @@ import { findValueListExceptionListItems } from './find_value_list_exception_lis
|
|||
import { findExceptionListsItemPointInTimeFinder } from './find_exception_list_items_point_in_time_finder';
|
||||
import { findValueListExceptionListItemsPointInTimeFinder } from './find_value_list_exception_list_items_point_in_time_finder';
|
||||
import { findExceptionListItemPointInTimeFinder } from './find_exception_list_item_point_in_time_finder';
|
||||
import { duplicateExceptionListAndItems } from './duplicate_exception_list';
|
||||
|
||||
/**
|
||||
* Class for use for exceptions that are with trusted applications or
|
||||
|
@ -311,6 +313,25 @@ export class ExceptionListClient {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the Trusted Apps Agnostic list if it does not yet exist (`null` is returned if it does exist)
|
||||
* @param options.listId the "list_id" of the exception list
|
||||
* @param options.namespaceType saved object namespace (single | agnostic)
|
||||
* @returns The exception list schema or null if it does not exist
|
||||
*/
|
||||
public duplicateExceptionListAndItems = async ({
|
||||
listId,
|
||||
namespaceType,
|
||||
}: DuplicateExceptionListOptions): Promise<ExceptionListSchema | null> => {
|
||||
const { savedObjectsClient, user } = this;
|
||||
return duplicateExceptionListAndItems({
|
||||
listId,
|
||||
namespaceType,
|
||||
savedObjectsClient,
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the same as "updateExceptionListItem" except it applies specifically to the endpoint list and will
|
||||
* auto-call the "createEndpointList" for you so that you have the best chance of the endpoint
|
||||
|
|
|
@ -287,6 +287,17 @@ export interface CreateEndpointListItemOptions {
|
|||
type: ExceptionListItemType;
|
||||
}
|
||||
|
||||
/**
|
||||
* ExceptionListClient.duplicateExceptionListAndItems
|
||||
* {@link ExceptionListClient.duplicateExceptionListAndItems}
|
||||
*/
|
||||
export interface DuplicateExceptionListOptions {
|
||||
/** The single list id to do the search against */
|
||||
listId: ListId;
|
||||
/** saved object namespace (single | agnostic) */
|
||||
namespaceType: NamespaceType;
|
||||
}
|
||||
|
||||
/**
|
||||
* ExceptionListClient.updateExceptionListItem
|
||||
* {@link ExceptionListClient.updateExceptionListItem}
|
||||
|
|
|
@ -133,6 +133,7 @@ describe('Perform bulk action request schema', () => {
|
|||
const payload: PerformBulkActionRequestBody = {
|
||||
query: 'name: test',
|
||||
action: BulkActionType.duplicate,
|
||||
[BulkActionType.duplicate]: { include_exceptions: false },
|
||||
};
|
||||
const message = retrieveValidationMessage(payload);
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
|
|
|
@ -131,6 +131,14 @@ export const BulkActionEditPayload = t.union([
|
|||
BulkActionEditPayloadSchedule,
|
||||
]);
|
||||
|
||||
const bulkActionDuplicatePayload = t.exact(
|
||||
t.type({
|
||||
include_exceptions: t.boolean,
|
||||
})
|
||||
);
|
||||
|
||||
export type BulkActionDuplicatePayload = t.TypeOf<typeof bulkActionDuplicatePayload>;
|
||||
|
||||
/**
|
||||
* actions that modify rules attributes
|
||||
*/
|
||||
|
@ -164,12 +172,23 @@ export const PerformBulkActionRequestBody = t.intersection([
|
|||
action: t.union([
|
||||
t.literal(BulkActionType.delete),
|
||||
t.literal(BulkActionType.disable),
|
||||
t.literal(BulkActionType.duplicate),
|
||||
t.literal(BulkActionType.enable),
|
||||
t.literal(BulkActionType.export),
|
||||
]),
|
||||
})
|
||||
),
|
||||
t.intersection([
|
||||
t.exact(
|
||||
t.type({
|
||||
action: t.literal(BulkActionType.duplicate),
|
||||
})
|
||||
),
|
||||
t.exact(
|
||||
t.partial({
|
||||
[BulkActionType.duplicate]: bulkActionDuplicatePayload,
|
||||
})
|
||||
),
|
||||
]),
|
||||
t.exact(
|
||||
t.type({
|
||||
action: t.literal(BulkActionType.edit),
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export enum DuplicateOptions {
|
||||
withExceptions = 'withExceptions',
|
||||
withoutExceptions = 'withoutExceptions',
|
||||
}
|
|
@ -22,6 +22,7 @@ export const EDIT_RULE_ACTION_BTN = '[data-test-subj="editRuleAction"]';
|
|||
export const DUPLICATE_RULE_ACTION_BTN = '[data-test-subj="duplicateRuleAction"]';
|
||||
|
||||
export const DUPLICATE_RULE_MENU_PANEL_BTN = '[data-test-subj="rules-details-duplicate-rule"]';
|
||||
export const CONFIRM_DUPLICATE_RULE = '[data-test-subj="confirmModalConfirmButton"]';
|
||||
|
||||
export const ENABLE_RULE_BULK_BTN = '[data-test-subj="enableRuleBulk"]';
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
DUPLICATE_RULE_ACTION_BTN,
|
||||
DUPLICATE_RULE_MENU_PANEL_BTN,
|
||||
DUPLICATE_RULE_BULK_BTN,
|
||||
CONFIRM_DUPLICATE_RULE,
|
||||
RULES_ROW,
|
||||
SELECT_ALL_RULES_BTN,
|
||||
MODAL_CONFIRMATION_BTN,
|
||||
|
@ -80,6 +81,7 @@ export const duplicateFirstRule = () => {
|
|||
cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true });
|
||||
cy.get(DUPLICATE_RULE_ACTION_BTN).should('be.visible');
|
||||
cy.get(DUPLICATE_RULE_ACTION_BTN).click();
|
||||
cy.get(CONFIRM_DUPLICATE_RULE).click();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -96,6 +98,7 @@ export const duplicateRuleFromMenu = () => {
|
|||
|
||||
// Because of a fade effect and fast clicking this can produce more than one click
|
||||
cy.get(DUPLICATE_RULE_MENU_PANEL_BTN).pipe(click);
|
||||
cy.get(CONFIRM_DUPLICATE_RULE).click();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -138,6 +141,7 @@ export const duplicateSelectedRules = () => {
|
|||
cy.log('Duplicate selected rules');
|
||||
cy.get(BULK_ACTIONS_BTN).click({ force: true });
|
||||
cy.get(DUPLICATE_RULE_BULK_BTN).click();
|
||||
cy.get(CONFIRM_DUPLICATE_RULE).click();
|
||||
};
|
||||
|
||||
export const enableSelectedRules = () => {
|
||||
|
|
|
@ -133,6 +133,8 @@ import { ExceptionsViewer } from '../../../rule_exceptions/components/all_except
|
|||
import type { NavTab } from '../../../../common/components/navigation/types';
|
||||
import { EditRuleSettingButtonLink } from '../../../../detections/pages/detection_engine/rules/details/components/edit_rule_settings_button_link';
|
||||
import { useStartMlJobs } from '../../../rule_management/logic/use_start_ml_jobs';
|
||||
import { useBulkDuplicateExceptionsConfirmation } from '../../../rule_management_ui/components/rules_table/bulk_actions/use_bulk_duplicate_confirmation';
|
||||
import { BulkActionDuplicateExceptionsConfirmation } from '../../../rule_management_ui/components/rules_table/bulk_actions/bulk_duplicate_exceptions_confirmation';
|
||||
|
||||
/**
|
||||
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
|
||||
|
@ -625,6 +627,13 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
|
||||
);
|
||||
|
||||
const {
|
||||
isBulkDuplicateConfirmationVisible,
|
||||
showBulkDuplicateConfirmation,
|
||||
cancelRuleDuplication,
|
||||
confirmRuleDuplication,
|
||||
} = useBulkDuplicateExceptionsConfirmation();
|
||||
|
||||
if (
|
||||
redirectToDetections(
|
||||
isSignalIndexExists,
|
||||
|
@ -646,6 +655,13 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
<>
|
||||
<NeedAdminForUpdateRulesCallOut />
|
||||
<MissingPrivilegesCallOut />
|
||||
{isBulkDuplicateConfirmationVisible && (
|
||||
<BulkActionDuplicateExceptionsConfirmation
|
||||
onCancel={cancelRuleDuplication}
|
||||
onConfirm={confirmRuleDuplication}
|
||||
rulesCount={1}
|
||||
/>
|
||||
)}
|
||||
<StyledFullHeightContainer onKeyDown={onKeyDown} ref={containerElement}>
|
||||
<EuiWindowEvent event="resize" handler={noop} />
|
||||
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
||||
|
@ -736,6 +752,7 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
rule,
|
||||
hasActionsPrivileges
|
||||
)}
|
||||
showBulkDuplicateExceptionsConfirmation={showBulkDuplicateConfirmation}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -28,7 +28,10 @@ import {
|
|||
import type { RulesReferencedByExceptionListsSchema } from '../../../../common/detection_engine/rule_exceptions';
|
||||
import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '../../../../common/detection_engine/rule_exceptions';
|
||||
|
||||
import type { BulkActionEditPayload } from '../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema';
|
||||
import type {
|
||||
BulkActionEditPayload,
|
||||
BulkActionDuplicatePayload,
|
||||
} from '../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema';
|
||||
import { BulkActionType } from '../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema';
|
||||
|
||||
import type {
|
||||
|
@ -215,13 +218,23 @@ export interface BulkActionResponse {
|
|||
|
||||
export type QueryOrIds = { query: string; ids?: undefined } | { query?: undefined; ids: string[] };
|
||||
type PlainBulkAction = {
|
||||
type: Exclude<BulkActionType, BulkActionType.edit | BulkActionType.export>;
|
||||
type: Exclude<
|
||||
BulkActionType,
|
||||
BulkActionType.edit | BulkActionType.export | BulkActionType.duplicate
|
||||
>;
|
||||
} & QueryOrIds;
|
||||
|
||||
type EditBulkAction = {
|
||||
type: BulkActionType.edit;
|
||||
editPayload: BulkActionEditPayload[];
|
||||
} & QueryOrIds;
|
||||
export type BulkAction = PlainBulkAction | EditBulkAction;
|
||||
|
||||
type DuplicateBulkAction = {
|
||||
type: BulkActionType.duplicate;
|
||||
duplicatePayload?: BulkActionDuplicatePayload;
|
||||
} & QueryOrIds;
|
||||
|
||||
export type BulkAction = PlainBulkAction | EditBulkAction | DuplicateBulkAction;
|
||||
|
||||
export interface PerformBulkActionProps {
|
||||
bulkAction: BulkAction;
|
||||
|
@ -245,6 +258,8 @@ export async function performBulkAction({
|
|||
query: bulkAction.query,
|
||||
ids: bulkAction.ids,
|
||||
edit: bulkAction.type === BulkActionType.edit ? bulkAction.editPayload : undefined,
|
||||
duplicate:
|
||||
bulkAction.type === BulkActionType.duplicate ? bulkAction.duplicatePayload : undefined,
|
||||
};
|
||||
|
||||
return KibanaServices.get().http.fetch<BulkActionResponse>(DETECTION_ENGINE_RULES_BULK_ACTION, {
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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, { useCallback, useState } from 'react';
|
||||
import { EuiRadioGroup, EuiText, EuiConfirmModal, EuiSpacer, EuiIconTip } from '@elastic/eui';
|
||||
import { DuplicateOptions } from '../../../../../../common/detection_engine/rule_management/constants';
|
||||
|
||||
import { bulkDuplicateRuleActions as i18n } from './translations';
|
||||
|
||||
interface BulkDuplicateExceptionsConfirmationProps {
|
||||
onCancel: () => void;
|
||||
onConfirm: (s: string) => void;
|
||||
rulesCount: number;
|
||||
}
|
||||
|
||||
const BulkActionDuplicateExceptionsConfirmationComponent = ({
|
||||
onCancel,
|
||||
onConfirm,
|
||||
rulesCount,
|
||||
}: BulkDuplicateExceptionsConfirmationProps) => {
|
||||
const [selectedDuplicateOption, setSelectedDuplicateOption] = useState(
|
||||
DuplicateOptions.withExceptions
|
||||
);
|
||||
|
||||
const handleRadioChange = useCallback(
|
||||
(optionId) => {
|
||||
setSelectedDuplicateOption(optionId);
|
||||
},
|
||||
[setSelectedDuplicateOption]
|
||||
);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onConfirm(selectedDuplicateOption);
|
||||
}, [onConfirm, selectedDuplicateOption]);
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
title={i18n.MODAL_TITLE(rulesCount)}
|
||||
onConfirm={handleConfirm}
|
||||
cancelButtonText={i18n.CANCEL_BUTTON}
|
||||
confirmButtonText={i18n.CONTINUE_BUTTON}
|
||||
defaultFocusedButton="confirm"
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<EuiText>
|
||||
{i18n.MODAL_TEXT(rulesCount)}{' '}
|
||||
<EuiIconTip content={i18n.DUPLICATE_TOOLTIP} position="bottom" />
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer />
|
||||
<EuiRadioGroup
|
||||
options={[
|
||||
{
|
||||
id: DuplicateOptions.withExceptions,
|
||||
label: i18n.DUPLICATE_EXCEPTIONS_TEXT(rulesCount),
|
||||
},
|
||||
{
|
||||
id: DuplicateOptions.withoutExceptions,
|
||||
label: i18n.DUPLICATE_WITHOUT_EXCEPTIONS_TEXT(rulesCount),
|
||||
},
|
||||
]}
|
||||
idSelected={selectedDuplicateOption}
|
||||
onChange={handleRadioChange}
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
export const BulkActionDuplicateExceptionsConfirmation = React.memo(
|
||||
BulkActionDuplicateExceptionsConfirmationComponent
|
||||
);
|
||||
|
||||
BulkActionDuplicateExceptionsConfirmation.displayName = 'BulkActionDuplicateExceptionsConfirmation';
|
|
@ -134,3 +134,59 @@ export const bulkSetSchedule = {
|
|||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const bulkDuplicateRuleActions = {
|
||||
MODAL_TITLE: (rulesCount: number): JSX.Element => (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalTitle"
|
||||
defaultMessage="Duplicate {rulesCount, plural, one {the rule} other {rules}} with exceptions?"
|
||||
values={{ rulesCount }}
|
||||
/>
|
||||
),
|
||||
|
||||
MODAL_TEXT: (rulesCount: number): JSX.Element => (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalBody"
|
||||
defaultMessage="You are duplicating {rulesCount, plural, one {# selected rule} other {# selected rules}}, please select how you would like to duplicate the existing exceptions"
|
||||
values={{ rulesCount }}
|
||||
/>
|
||||
),
|
||||
|
||||
DUPLICATE_EXCEPTIONS_TEXT: (rulesCount: number) => (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.with"
|
||||
defaultMessage="Duplicate {rulesCount, plural, one {rule} other {rules}} and their exceptions"
|
||||
values={{ rulesCount }}
|
||||
/>
|
||||
),
|
||||
|
||||
DUPLICATE_WITHOUT_EXCEPTIONS_TEXT: (rulesCount: number) => (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.without"
|
||||
defaultMessage="Only duplicate {rulesCount, plural, one {the rule} other {rules}}"
|
||||
values={{ rulesCount }}
|
||||
/>
|
||||
),
|
||||
|
||||
CONTINUE_BUTTON: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.continueButton',
|
||||
{
|
||||
defaultMessage: 'Duplicate',
|
||||
}
|
||||
),
|
||||
|
||||
CANCEL_BUTTON: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.cancelButton',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
),
|
||||
|
||||
DUPLICATE_TOOLTIP: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.tooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
' If you duplicate exceptions, then the shared exceptions list will be duplicated by reference and the default rule exception will be copied and created as a new one',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { Toast } from '@kbn/core/public';
|
|||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import React, { useCallback } from 'react';
|
||||
import { DuplicateOptions } from '../../../../../../common/detection_engine/rule_management/constants';
|
||||
import type { BulkActionEditPayload } from '../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema';
|
||||
import {
|
||||
BulkActionType,
|
||||
|
@ -46,6 +47,7 @@ interface UseBulkActionsArgs {
|
|||
result: DryRunResult | undefined,
|
||||
action: BulkActionForConfirmation
|
||||
) => Promise<boolean>;
|
||||
showBulkDuplicateConfirmation: () => Promise<string | null>;
|
||||
completeBulkEditForm: (
|
||||
bulkActionEditType: BulkActionEditType
|
||||
) => Promise<BulkActionEditPayload | null>;
|
||||
|
@ -56,6 +58,7 @@ export const useBulkActions = ({
|
|||
filterOptions,
|
||||
confirmDeletion,
|
||||
showBulkActionConfirmation,
|
||||
showBulkDuplicateConfirmation,
|
||||
completeBulkEditForm,
|
||||
executeBulkActionsDryRun,
|
||||
}: UseBulkActionsArgs) => {
|
||||
|
@ -125,8 +128,16 @@ export const useBulkActions = ({
|
|||
startTransaction({ name: BULK_RULE_ACTIONS.DUPLICATE });
|
||||
closePopover();
|
||||
|
||||
const modalDuplicationConfirmationResult = await showBulkDuplicateConfirmation();
|
||||
if (modalDuplicationConfirmationResult === null) {
|
||||
return;
|
||||
}
|
||||
await executeBulkAction({
|
||||
type: BulkActionType.duplicate,
|
||||
duplicatePayload: {
|
||||
include_exceptions:
|
||||
modalDuplicationConfirmationResult === DuplicateOptions.withExceptions,
|
||||
},
|
||||
...(isAllSelected ? { query: filterQuery } : { ids: selectedRuleIds }),
|
||||
});
|
||||
clearRulesSelection();
|
||||
|
@ -461,6 +472,7 @@ export const useBulkActions = ({
|
|||
filterOptions,
|
||||
completeBulkEditForm,
|
||||
downloadExportedRules,
|
||||
showBulkDuplicateConfirmation,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { useCallback, useRef } from 'react';
|
||||
|
||||
import { useBoolState } from '../../../../../common/hooks/use_bool_state';
|
||||
|
||||
/**
|
||||
* hook that controls bulk duplicate actions exceptions confirmation modal window and its content
|
||||
*/
|
||||
export const useBulkDuplicateExceptionsConfirmation = () => {
|
||||
const [isBulkDuplicateConfirmationVisible, showModal, hideModal] = useBoolState();
|
||||
const confirmationPromiseRef = useRef<(result: string | null) => void>();
|
||||
|
||||
const onConfirm = useCallback((value: string) => {
|
||||
confirmationPromiseRef.current?.(value);
|
||||
}, []);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
confirmationPromiseRef.current?.(null);
|
||||
}, []);
|
||||
|
||||
const initModal = useCallback(() => {
|
||||
showModal();
|
||||
|
||||
return new Promise<string | null>((resolve) => {
|
||||
confirmationPromiseRef.current = resolve;
|
||||
}).finally(() => {
|
||||
hideModal();
|
||||
});
|
||||
}, [showModal, hideModal]);
|
||||
|
||||
const showBulkDuplicateConfirmation = useCallback(async () => {
|
||||
const confirmation = await initModal();
|
||||
if (confirmation) {
|
||||
onConfirm(confirmation);
|
||||
} else {
|
||||
onCancel();
|
||||
}
|
||||
|
||||
return confirmation;
|
||||
}, [initModal, onConfirm, onCancel]);
|
||||
|
||||
return {
|
||||
isBulkDuplicateConfirmationVisible,
|
||||
showBulkDuplicateConfirmation,
|
||||
cancelRuleDuplication: onCancel,
|
||||
confirmRuleDuplication: onConfirm,
|
||||
};
|
||||
};
|
|
@ -36,6 +36,8 @@ import { RulesTableUtilityBar } from './rules_table_utility_bar';
|
|||
import { useMonitoringColumns, useRulesColumns } from './use_columns';
|
||||
import { useUserData } from '../../../../detections/components/user_info';
|
||||
import { hasUserCRUDPermission } from '../../../../common/utils/privileges';
|
||||
import { useBulkDuplicateExceptionsConfirmation } from './bulk_actions/use_bulk_duplicate_confirmation';
|
||||
import { BulkActionDuplicateExceptionsConfirmation } from './bulk_actions/bulk_duplicate_exceptions_confirmation';
|
||||
import { useStartMlJobs } from '../../../rule_management/logic/use_start_ml_jobs';
|
||||
|
||||
const INITIAL_SORT_FIELD = 'enabled';
|
||||
|
@ -101,6 +103,13 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
|
|||
approveBulkActionConfirmation,
|
||||
} = useBulkActionsConfirmation();
|
||||
|
||||
const {
|
||||
isBulkDuplicateConfirmationVisible,
|
||||
showBulkDuplicateConfirmation,
|
||||
cancelRuleDuplication,
|
||||
confirmRuleDuplication,
|
||||
} = useBulkDuplicateExceptionsConfirmation();
|
||||
|
||||
const {
|
||||
bulkEditActionType,
|
||||
isBulkEditFlyoutVisible,
|
||||
|
@ -115,6 +124,7 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
|
|||
filterOptions,
|
||||
confirmDeletion,
|
||||
showBulkActionConfirmation,
|
||||
showBulkDuplicateConfirmation,
|
||||
completeBulkEditForm,
|
||||
executeBulkActionsDryRun,
|
||||
});
|
||||
|
@ -147,12 +157,14 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
|
|||
isLoadingJobs,
|
||||
mlJobs,
|
||||
startMlJobs,
|
||||
showExceptionsDuplicateConfirmation: showBulkDuplicateConfirmation,
|
||||
});
|
||||
const monitoringColumns = useMonitoringColumns({
|
||||
hasCRUDPermissions: hasPermissions,
|
||||
isLoadingJobs,
|
||||
mlJobs,
|
||||
startMlJobs,
|
||||
showExceptionsDuplicateConfirmation: showBulkDuplicateConfirmation,
|
||||
});
|
||||
|
||||
const isSelectAllCalled = useRef(false);
|
||||
|
@ -259,6 +271,13 @@ export const RulesTables = React.memo<RulesTableProps>(({ selectedTab }) => {
|
|||
onConfirm={approveBulkActionConfirmation}
|
||||
/>
|
||||
)}
|
||||
{isBulkDuplicateConfirmationVisible && (
|
||||
<BulkActionDuplicateExceptionsConfirmation
|
||||
onCancel={cancelRuleDuplication}
|
||||
onConfirm={confirmRuleDuplication}
|
||||
rulesCount={selectedRuleIds?.length ? selectedRuleIds?.length : 1}
|
||||
/>
|
||||
)}
|
||||
{isBulkEditFlyoutVisible && bulkEditActionType !== undefined && (
|
||||
<BulkEditFlyout
|
||||
rulesCount={bulkActionsDryRunResult?.succeededRulesCount ?? 0}
|
||||
|
|
|
@ -55,6 +55,10 @@ interface ColumnsProps {
|
|||
startMlJobs: (jobIds: string[] | undefined) => Promise<void>;
|
||||
}
|
||||
|
||||
interface ActionColumnsProps {
|
||||
showExceptionsDuplicateConfirmation: () => Promise<string | null>;
|
||||
}
|
||||
|
||||
const useEnabledColumn = ({ hasCRUDPermissions, startMlJobs }: ColumnsProps): TableColumn => {
|
||||
const hasMlPermissions = useHasMlPermissions();
|
||||
const hasActionsPrivileges = useHasActionsPrivileges();
|
||||
|
@ -209,19 +213,24 @@ const INTEGRATIONS_COLUMN: TableColumn = {
|
|||
truncateText: true,
|
||||
};
|
||||
|
||||
const useActionsColumn = (): EuiTableActionsColumnType<Rule> => {
|
||||
const actions = useRulesTableActions();
|
||||
const useActionsColumn = ({
|
||||
showExceptionsDuplicateConfirmation,
|
||||
}: ActionColumnsProps): EuiTableActionsColumnType<Rule> => {
|
||||
const actions = useRulesTableActions({ showExceptionsDuplicateConfirmation });
|
||||
|
||||
return useMemo(() => ({ actions, width: '40px' }), [actions]);
|
||||
};
|
||||
|
||||
export interface UseColumnsProps extends ColumnsProps, ActionColumnsProps {}
|
||||
|
||||
export const useRulesColumns = ({
|
||||
hasCRUDPermissions,
|
||||
isLoadingJobs,
|
||||
mlJobs,
|
||||
startMlJobs,
|
||||
}: ColumnsProps): TableColumn[] => {
|
||||
const actionsColumn = useActionsColumn();
|
||||
showExceptionsDuplicateConfirmation,
|
||||
}: UseColumnsProps): TableColumn[] => {
|
||||
const actionsColumn = useActionsColumn({ showExceptionsDuplicateConfirmation });
|
||||
const ruleNameColumn = useRuleNameColumn();
|
||||
const { isInMemorySorting } = useRulesTableContext().state;
|
||||
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
|
||||
|
@ -338,9 +347,10 @@ export const useMonitoringColumns = ({
|
|||
isLoadingJobs,
|
||||
mlJobs,
|
||||
startMlJobs,
|
||||
}: ColumnsProps): TableColumn[] => {
|
||||
showExceptionsDuplicateConfirmation,
|
||||
}: UseColumnsProps): TableColumn[] => {
|
||||
const docLinks = useKibana().services.docLinks;
|
||||
const actionsColumn = useActionsColumn();
|
||||
const actionsColumn = useActionsColumn({ showExceptionsDuplicateConfirmation });
|
||||
const ruleNameColumn = useRuleNameColumn();
|
||||
const { isInMemorySorting } = useRulesTableContext().state;
|
||||
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import type { DefaultItemAction } from '@elastic/eui';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { DuplicateOptions } from '../../../../../common/detection_engine/rule_management/constants';
|
||||
import { BulkActionType } from '../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema';
|
||||
import { SINGLE_RULE_ACTIONS } from '../../../../common/lib/apm/user_actions';
|
||||
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
|
||||
|
@ -23,7 +24,11 @@ import {
|
|||
import { useDownloadExportedRules } from '../../../rule_management/logic/bulk_actions/use_download_exported_rules';
|
||||
import { useHasActionsPrivileges } from './use_has_actions_privileges';
|
||||
|
||||
export const useRulesTableActions = (): Array<DefaultItemAction<Rule>> => {
|
||||
export const useRulesTableActions = ({
|
||||
showExceptionsDuplicateConfirmation,
|
||||
}: {
|
||||
showExceptionsDuplicateConfirmation: () => Promise<string | null>;
|
||||
}): Array<DefaultItemAction<Rule>> => {
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
const hasActionsPrivileges = useHasActionsPrivileges();
|
||||
const { startTransaction } = useStartTransaction();
|
||||
|
@ -63,9 +68,17 @@ export const useRulesTableActions = (): Array<DefaultItemAction<Rule>> => {
|
|||
// TODO extract those handlers to hooks, like useDuplicateRule
|
||||
onClick: async (rule: Rule) => {
|
||||
startTransaction({ name: SINGLE_RULE_ACTIONS.DUPLICATE });
|
||||
const modalDuplicationConfirmationResult = await showExceptionsDuplicateConfirmation();
|
||||
if (modalDuplicationConfirmationResult === null) {
|
||||
return;
|
||||
}
|
||||
const result = await executeBulkAction({
|
||||
type: BulkActionType.duplicate,
|
||||
ids: [rule.id],
|
||||
duplicatePayload: {
|
||||
include_exceptions:
|
||||
modalDuplicationConfirmationResult === DuplicateOptions.withExceptions,
|
||||
},
|
||||
});
|
||||
const createdRules = result?.attributes.results.created;
|
||||
if (createdRules?.length) {
|
||||
|
|
|
@ -15,6 +15,8 @@ import { RuleActionsOverflow } from '.';
|
|||
import { mockRule } from '../../../../detection_engine/rule_management_ui/components/rules_table/__mocks__/mock';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
|
||||
const showBulkDuplicateExceptionsConfirmation = () => Promise.resolve(null);
|
||||
|
||||
jest.mock(
|
||||
'../../../../detection_engine/rule_management/logic/bulk_actions/use_execute_bulk_action'
|
||||
);
|
||||
|
@ -50,6 +52,7 @@ describe('RuleActionsOverflow', () => {
|
|||
test('menu items rendered when a rule is passed to the component', () => {
|
||||
const { getByTestId } = render(
|
||||
<RuleActionsOverflow
|
||||
showBulkDuplicateExceptionsConfirmation={showBulkDuplicateExceptionsConfirmation}
|
||||
rule={mockRule('id')}
|
||||
userHasPermissions
|
||||
canDuplicateRuleWithActions={true}
|
||||
|
@ -64,7 +67,12 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
test('menu is empty when no rule is passed to the component', () => {
|
||||
const { getByTestId } = render(
|
||||
<RuleActionsOverflow rule={null} userHasPermissions canDuplicateRuleWithActions={true} />,
|
||||
<RuleActionsOverflow
|
||||
showBulkDuplicateExceptionsConfirmation={showBulkDuplicateExceptionsConfirmation}
|
||||
rule={null}
|
||||
userHasPermissions
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>,
|
||||
{ wrapper: TestProviders }
|
||||
);
|
||||
fireEvent.click(getByTestId('rules-details-popover-button-icon'));
|
||||
|
@ -76,6 +84,7 @@ describe('RuleActionsOverflow', () => {
|
|||
test('it does not open the popover when rules-details-popover-button-icon is clicked when the user does not have permission', () => {
|
||||
const { getByTestId } = render(
|
||||
<RuleActionsOverflow
|
||||
showBulkDuplicateExceptionsConfirmation={showBulkDuplicateExceptionsConfirmation}
|
||||
rule={mockRule('id')}
|
||||
userHasPermissions={false}
|
||||
canDuplicateRuleWithActions={true}
|
||||
|
@ -92,6 +101,7 @@ describe('RuleActionsOverflow', () => {
|
|||
test('it closes the popover when rules-details-duplicate-rule is clicked', () => {
|
||||
const { getByTestId } = render(
|
||||
<RuleActionsOverflow
|
||||
showBulkDuplicateExceptionsConfirmation={showBulkDuplicateExceptionsConfirmation}
|
||||
rule={mockRule('id')}
|
||||
userHasPermissions
|
||||
canDuplicateRuleWithActions={true}
|
||||
|
@ -103,44 +113,6 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
expect(getByTestId('rules-details-popover')).not.toHaveTextContent(/.+/);
|
||||
});
|
||||
|
||||
test('it calls duplicate action when rules-details-duplicate-rule is clicked', () => {
|
||||
const executeBulkAction = jest.fn();
|
||||
useExecuteBulkActionMock.mockReturnValue({ executeBulkAction });
|
||||
|
||||
const { getByTestId } = render(
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasPermissions
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>,
|
||||
{ wrapper: TestProviders }
|
||||
);
|
||||
fireEvent.click(getByTestId('rules-details-popover-button-icon'));
|
||||
fireEvent.click(getByTestId('rules-details-duplicate-rule'));
|
||||
|
||||
expect(executeBulkAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'duplicate' })
|
||||
);
|
||||
});
|
||||
|
||||
test('it calls duplicate action with the rule and rule.id when rules-details-duplicate-rule is clicked', () => {
|
||||
const executeBulkAction = jest.fn();
|
||||
useExecuteBulkActionMock.mockReturnValue({ executeBulkAction });
|
||||
|
||||
const { getByTestId } = render(
|
||||
<RuleActionsOverflow
|
||||
rule={mockRule('id')}
|
||||
userHasPermissions
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>,
|
||||
{ wrapper: TestProviders }
|
||||
);
|
||||
fireEvent.click(getByTestId('rules-details-popover-button-icon'));
|
||||
fireEvent.click(getByTestId('rules-details-duplicate-rule'));
|
||||
|
||||
expect(executeBulkAction).toHaveBeenCalledWith({ type: 'duplicate', ids: ['id'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('rules details export rule', () => {
|
||||
|
@ -150,6 +122,7 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
const { getByTestId } = render(
|
||||
<RuleActionsOverflow
|
||||
showBulkDuplicateExceptionsConfirmation={showBulkDuplicateExceptionsConfirmation}
|
||||
rule={mockRule('id')}
|
||||
userHasPermissions
|
||||
canDuplicateRuleWithActions={true}
|
||||
|
@ -165,6 +138,7 @@ describe('RuleActionsOverflow', () => {
|
|||
test('it closes the popover when rules-details-export-rule is clicked', () => {
|
||||
const { getByTestId } = render(
|
||||
<RuleActionsOverflow
|
||||
showBulkDuplicateExceptionsConfirmation={showBulkDuplicateExceptionsConfirmation}
|
||||
rule={mockRule('id')}
|
||||
userHasPermissions
|
||||
canDuplicateRuleWithActions={true}
|
||||
|
@ -183,6 +157,7 @@ describe('RuleActionsOverflow', () => {
|
|||
test('it closes the popover when rules-details-delete-rule is clicked', () => {
|
||||
const { getByTestId } = render(
|
||||
<RuleActionsOverflow
|
||||
showBulkDuplicateExceptionsConfirmation={showBulkDuplicateExceptionsConfirmation}
|
||||
rule={mockRule('id')}
|
||||
userHasPermissions
|
||||
canDuplicateRuleWithActions={true}
|
||||
|
@ -202,6 +177,7 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
const { getByTestId } = render(
|
||||
<RuleActionsOverflow
|
||||
showBulkDuplicateExceptionsConfirmation={showBulkDuplicateExceptionsConfirmation}
|
||||
rule={mockRule('id')}
|
||||
userHasPermissions
|
||||
canDuplicateRuleWithActions={true}
|
||||
|
@ -220,7 +196,12 @@ describe('RuleActionsOverflow', () => {
|
|||
|
||||
const rule = mockRule('id');
|
||||
const { getByTestId } = render(
|
||||
<RuleActionsOverflow rule={rule} userHasPermissions canDuplicateRuleWithActions={true} />,
|
||||
<RuleActionsOverflow
|
||||
showBulkDuplicateExceptionsConfirmation={showBulkDuplicateExceptionsConfirmation}
|
||||
rule={rule}
|
||||
userHasPermissions
|
||||
canDuplicateRuleWithActions={true}
|
||||
/>,
|
||||
{ wrapper: TestProviders }
|
||||
);
|
||||
fireEvent.click(getByTestId('rules-details-popover-button-icon'));
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants';
|
||||
import { DuplicateOptions } from '../../../../../common/detection_engine/rule_management/constants';
|
||||
import { BulkActionType } from '../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema';
|
||||
import { getRulesUrl } from '../../../../common/components/link_to/redirect_to_detection_engine';
|
||||
import { useBoolState } from '../../../../common/hooks/use_bool_state';
|
||||
|
@ -47,6 +48,7 @@ interface RuleActionsOverflowComponentProps {
|
|||
rule: Rule | null;
|
||||
userHasPermissions: boolean;
|
||||
canDuplicateRuleWithActions: boolean;
|
||||
showBulkDuplicateExceptionsConfirmation: () => Promise<string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,6 +58,7 @@ const RuleActionsOverflowComponent = ({
|
|||
rule,
|
||||
userHasPermissions,
|
||||
canDuplicateRuleWithActions,
|
||||
showBulkDuplicateExceptionsConfirmation,
|
||||
}: RuleActionsOverflowComponentProps) => {
|
||||
const [isPopoverOpen, , closePopover, togglePopover] = useBoolState();
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
|
@ -83,10 +86,20 @@ const RuleActionsOverflowComponent = ({
|
|||
onClick={async () => {
|
||||
startTransaction({ name: SINGLE_RULE_ACTIONS.DUPLICATE });
|
||||
closePopover();
|
||||
const modalDuplicationConfirmationResult =
|
||||
await showBulkDuplicateExceptionsConfirmation();
|
||||
if (modalDuplicationConfirmationResult === null) {
|
||||
return;
|
||||
}
|
||||
const result = await executeBulkAction({
|
||||
type: BulkActionType.duplicate,
|
||||
ids: [rule.id],
|
||||
duplicatePayload: {
|
||||
include_exceptions:
|
||||
modalDuplicationConfirmationResult === DuplicateOptions.withExceptions,
|
||||
},
|
||||
});
|
||||
|
||||
const createdRules = result?.attributes.results.created;
|
||||
if (createdRules?.length) {
|
||||
goToRuleEditPage(createdRules[0].id, navigateToApp);
|
||||
|
@ -148,6 +161,7 @@ const RuleActionsOverflowComponent = ({
|
|||
navigateToApp,
|
||||
onRuleDeletedCallback,
|
||||
rule,
|
||||
showBulkDuplicateExceptionsConfirmation,
|
||||
startTransaction,
|
||||
userHasPermissions,
|
||||
downloadExportedRules,
|
||||
|
|
|
@ -95,56 +95,7 @@ export const createRuleExceptionsRoute = (router: SecuritySolutionPluginRouter)
|
|||
});
|
||||
}
|
||||
|
||||
let createdItems;
|
||||
|
||||
const ruleDefaultLists = rule.params.exceptionsList.filter(
|
||||
(list) => list.type === ExceptionListTypeEnum.RULE_DEFAULT
|
||||
);
|
||||
|
||||
// This should hopefully never happen, but could if we forget to add such a check to one
|
||||
// of our routes allowing the user to update the rule to have more than one default list added
|
||||
checkDefaultRuleExceptionListReferences({ exceptionLists: rule.params.exceptionsList });
|
||||
|
||||
const [ruleDefaultList] = ruleDefaultLists;
|
||||
|
||||
if (ruleDefaultList != null) {
|
||||
// check that list does indeed exist
|
||||
const exceptionListAssociatedToRule = await listsClient?.getExceptionList({
|
||||
id: ruleDefaultList.id,
|
||||
listId: ruleDefaultList.list_id,
|
||||
namespaceType: ruleDefaultList.namespace_type,
|
||||
});
|
||||
|
||||
// if list does exist, just need to create the items
|
||||
if (exceptionListAssociatedToRule != null) {
|
||||
createdItems = await createExceptionListItems({
|
||||
items,
|
||||
defaultList: exceptionListAssociatedToRule,
|
||||
listsClient,
|
||||
});
|
||||
} else {
|
||||
// This means that there was missed cleanup when this rule exception list was
|
||||
// deleted and it remained referenced on the rule. Let's remove it from the rule,
|
||||
// and update the rule's exceptions lists to include newly created default list.
|
||||
const defaultList = await createAndAssociateDefaultExceptionList({
|
||||
rule,
|
||||
rulesClient,
|
||||
listsClient,
|
||||
removeOldAssociation: true,
|
||||
});
|
||||
|
||||
createdItems = await createExceptionListItems({ items, defaultList, listsClient });
|
||||
}
|
||||
} else {
|
||||
const defaultList = await createAndAssociateDefaultExceptionList({
|
||||
rule,
|
||||
rulesClient,
|
||||
listsClient,
|
||||
removeOldAssociation: false,
|
||||
});
|
||||
|
||||
createdItems = await createExceptionListItems({ items, defaultList, listsClient });
|
||||
}
|
||||
const createdItems = await createRuleExceptions({ items, rule, listsClient, rulesClient });
|
||||
|
||||
const [validated, errors] = validate(createdItems, t.array(exceptionListItemSchema));
|
||||
if (errors != null) {
|
||||
|
@ -163,6 +114,67 @@ export const createRuleExceptionsRoute = (router: SecuritySolutionPluginRouter)
|
|||
);
|
||||
};
|
||||
|
||||
export const createRuleExceptions = async ({
|
||||
items,
|
||||
rule,
|
||||
listsClient,
|
||||
rulesClient,
|
||||
}: {
|
||||
items: CreateRuleExceptionListItemSchemaDecoded[];
|
||||
listsClient: ExceptionListClient | null;
|
||||
rulesClient: RulesClient;
|
||||
rule: SanitizedRule<RuleParams>;
|
||||
}) => {
|
||||
const ruleDefaultLists = rule.params.exceptionsList.filter(
|
||||
(list) => list.type === ExceptionListTypeEnum.RULE_DEFAULT
|
||||
);
|
||||
|
||||
// This should hopefully never happen, but could if we forget to add such a check to one
|
||||
// of our routes allowing the user to update the rule to have more than one default list added
|
||||
checkDefaultRuleExceptionListReferences({ exceptionLists: rule.params.exceptionsList });
|
||||
|
||||
const [ruleDefaultList] = ruleDefaultLists;
|
||||
|
||||
if (ruleDefaultList != null) {
|
||||
// check that list does indeed exist
|
||||
const exceptionListAssociatedToRule = await listsClient?.getExceptionList({
|
||||
id: ruleDefaultList.id,
|
||||
listId: ruleDefaultList.list_id,
|
||||
namespaceType: ruleDefaultList.namespace_type,
|
||||
});
|
||||
|
||||
// if list does exist, just need to create the items
|
||||
if (exceptionListAssociatedToRule != null) {
|
||||
return createExceptionListItems({
|
||||
items,
|
||||
defaultList: exceptionListAssociatedToRule,
|
||||
listsClient,
|
||||
});
|
||||
} else {
|
||||
// This means that there was missed cleanup when this rule exception list was
|
||||
// deleted and it remained referenced on the rule. Let's remove it from the rule,
|
||||
// and update the rule's exceptions lists to include newly created default list.
|
||||
const defaultList = await createAndAssociateDefaultExceptionList({
|
||||
rule,
|
||||
rulesClient,
|
||||
listsClient,
|
||||
removeOldAssociation: true,
|
||||
});
|
||||
|
||||
return createExceptionListItems({ items, defaultList, listsClient });
|
||||
}
|
||||
} else {
|
||||
const defaultList = await createAndAssociateDefaultExceptionList({
|
||||
rule,
|
||||
rulesClient,
|
||||
listsClient,
|
||||
removeOldAssociation: false,
|
||||
});
|
||||
|
||||
return createExceptionListItems({ items, defaultList, listsClient });
|
||||
}
|
||||
};
|
||||
|
||||
export const createExceptionListItems = async ({
|
||||
items,
|
||||
defaultList,
|
||||
|
@ -191,17 +203,15 @@ export const createExceptionListItems = async ({
|
|||
);
|
||||
};
|
||||
|
||||
export const createAndAssociateDefaultExceptionList = async ({
|
||||
export const createExceptionList = async ({
|
||||
rule,
|
||||
listsClient,
|
||||
rulesClient,
|
||||
removeOldAssociation,
|
||||
}: {
|
||||
rule: SanitizedRule<RuleParams>;
|
||||
listsClient: ExceptionListClient | null;
|
||||
rulesClient: RulesClient;
|
||||
removeOldAssociation: boolean;
|
||||
}): Promise<ExceptionListSchema> => {
|
||||
}): Promise<ExceptionListSchema | null> => {
|
||||
if (!listsClient) return null;
|
||||
|
||||
const exceptionList: CreateExceptionListSchema = {
|
||||
description: `Exception list containing exceptions for rule with id: ${rule.id}`,
|
||||
meta: undefined,
|
||||
|
@ -233,7 +243,7 @@ export const createAndAssociateDefaultExceptionList = async ({
|
|||
} = validated;
|
||||
|
||||
// create the default rule list
|
||||
const exceptionListAssociatedToRule = await listsClient?.createExceptionList({
|
||||
return listsClient.createExceptionList({
|
||||
description,
|
||||
immutable: false,
|
||||
listId,
|
||||
|
@ -244,8 +254,22 @@ export const createAndAssociateDefaultExceptionList = async ({
|
|||
type,
|
||||
version,
|
||||
});
|
||||
};
|
||||
|
||||
if (exceptionListAssociatedToRule == null) {
|
||||
export const createAndAssociateDefaultExceptionList = async ({
|
||||
rule,
|
||||
listsClient,
|
||||
rulesClient,
|
||||
removeOldAssociation,
|
||||
}: {
|
||||
rule: SanitizedRule<RuleParams>;
|
||||
listsClient: ExceptionListClient | null;
|
||||
rulesClient: RulesClient;
|
||||
removeOldAssociation: boolean;
|
||||
}): Promise<ExceptionListSchema> => {
|
||||
const exceptionListToAssociate = await createExceptionList({ rule, listsClient });
|
||||
|
||||
if (exceptionListToAssociate == null) {
|
||||
throw Error(`An error occurred creating rule default exception list`);
|
||||
}
|
||||
|
||||
|
@ -265,14 +289,14 @@ export const createAndAssociateDefaultExceptionList = async ({
|
|||
exceptions_list: [
|
||||
...ruleExceptionLists,
|
||||
{
|
||||
id: exceptionListAssociatedToRule.id,
|
||||
list_id: exceptionListAssociatedToRule.list_id,
|
||||
type: exceptionListAssociatedToRule.type,
|
||||
namespace_type: exceptionListAssociatedToRule.namespace_type,
|
||||
id: exceptionListToAssociate.id,
|
||||
list_id: exceptionListToAssociate.list_id,
|
||||
type: exceptionListToAssociate.type,
|
||||
namespace_type: exceptionListToAssociate.namespace_type,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return exceptionListAssociatedToRule;
|
||||
return exceptionListToAssociate;
|
||||
};
|
||||
|
|
|
@ -35,6 +35,7 @@ import { initPromisePool } from '../../../../../../utils/promise_pool';
|
|||
import { buildMlAuthz } from '../../../../../machine_learning/authz';
|
||||
import { deleteRules } from '../../../logic/crud/delete_rules';
|
||||
import { duplicateRule } from '../../../logic/actions/duplicate_rule';
|
||||
import { duplicateExceptions } from '../../../logic/actions/duplicate_exceptions';
|
||||
import { findRules } from '../../../logic/search/find_rules';
|
||||
import { readRules } from '../../../logic/crud/read_rules';
|
||||
import { getExportByObjectIds } from '../../../logic/export/get_export_by_object_ids';
|
||||
|
@ -497,18 +498,46 @@ export const performBulkActionRoute = (
|
|||
if (isDryRun) {
|
||||
return rule;
|
||||
}
|
||||
|
||||
const migratedRule = await migrateRuleActions({
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
rule,
|
||||
});
|
||||
let shouldDuplicateExceptions = true;
|
||||
if (body.duplicate !== undefined) {
|
||||
shouldDuplicateExceptions = body.duplicate.include_exceptions;
|
||||
}
|
||||
|
||||
const createdRule = await rulesClient.create({
|
||||
data: duplicateRule(migratedRule),
|
||||
const duplicateRuleToCreate = await duplicateRule({
|
||||
rule: migratedRule,
|
||||
});
|
||||
|
||||
return createdRule;
|
||||
const createdRule = await rulesClient.create({
|
||||
data: duplicateRuleToCreate,
|
||||
});
|
||||
|
||||
// we try to create exceptions after rule created, and then update rule
|
||||
const exceptions = shouldDuplicateExceptions
|
||||
? await duplicateExceptions({
|
||||
ruleId: rule.params.ruleId,
|
||||
exceptionLists: rule.params.exceptionsList,
|
||||
exceptionsClient,
|
||||
})
|
||||
: [];
|
||||
|
||||
const updatedRule = await rulesClient.update({
|
||||
id: createdRule.id,
|
||||
data: {
|
||||
...duplicateRuleToCreate,
|
||||
params: {
|
||||
...duplicateRuleToCreate.params,
|
||||
exceptionsList: exceptions,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: figureout why types can't return just updatedRule
|
||||
return { ...createdRule, ...updatedRule };
|
||||
},
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 type { ExceptionListClient } from '@kbn/lists-plugin/server';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import type { RuleParams } from '../../../rule_schema';
|
||||
|
||||
interface DuplicateExceptionsParams {
|
||||
ruleId: RuleParams['ruleId'];
|
||||
exceptionLists: RuleParams['exceptionsList'];
|
||||
exceptionsClient: ExceptionListClient | undefined;
|
||||
}
|
||||
|
||||
export const duplicateExceptions = async ({
|
||||
ruleId,
|
||||
exceptionLists,
|
||||
exceptionsClient,
|
||||
}: DuplicateExceptionsParams): Promise<RuleParams['exceptionsList']> => {
|
||||
if (exceptionLists == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Sort the shared lists and the rule_default lists.
|
||||
// Only a single rule_default list should exist per rule.
|
||||
const ruleDefaultList = exceptionLists.find(
|
||||
(list) => list.type === ExceptionListTypeEnum.RULE_DEFAULT
|
||||
);
|
||||
const sharedLists = exceptionLists.filter(
|
||||
(list) => list.type !== ExceptionListTypeEnum.RULE_DEFAULT
|
||||
);
|
||||
|
||||
// For rule_default list (exceptions that live only on a single rule), we need
|
||||
// to create a new rule_default list to assign to duplicated rule
|
||||
if (ruleDefaultList != null && exceptionsClient != null) {
|
||||
const ruleDefaultExceptionList = await exceptionsClient.duplicateExceptionListAndItems({
|
||||
listId: ruleDefaultList.list_id,
|
||||
namespaceType: ruleDefaultList.namespace_type,
|
||||
});
|
||||
|
||||
if (ruleDefaultExceptionList == null) {
|
||||
throw new Error(`Unable to duplicate rule default exception items for rule_id: ${ruleId}`);
|
||||
}
|
||||
|
||||
return [
|
||||
...sharedLists,
|
||||
{
|
||||
id: ruleDefaultExceptionList.id,
|
||||
list_id: ruleDefaultExceptionList.list_id,
|
||||
namespace_type: ruleDefaultExceptionList.namespace_type,
|
||||
type: ruleDefaultExceptionList.type,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// If no rule_default list exists, we can just return
|
||||
return [...sharedLists];
|
||||
};
|
|
@ -90,9 +90,11 @@ describe('duplicateRule', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns an object with fields copied from a given rule', () => {
|
||||
it('returns an object with fields copied from a given rule', async () => {
|
||||
const rule = createTestRule();
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
name: expect.anything(), // covered in a separate test
|
||||
|
@ -111,10 +113,12 @@ describe('duplicateRule', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('appends [Duplicate] to the name', () => {
|
||||
it('appends [Duplicate] to the name', async () => {
|
||||
const rule = createTestRule();
|
||||
rule.name = 'PowerShell Keylogging Script';
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -123,9 +127,11 @@ describe('duplicateRule', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('generates a new ruleId', () => {
|
||||
it('generates a new ruleId', async () => {
|
||||
const rule = createTestRule();
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -136,10 +142,12 @@ describe('duplicateRule', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('makes sure the duplicated rule is disabled', () => {
|
||||
it('makes sure the duplicated rule is disabled', async () => {
|
||||
const rule = createTestRule();
|
||||
rule.enabled = true;
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -155,9 +163,11 @@ describe('duplicateRule', () => {
|
|||
return rule;
|
||||
};
|
||||
|
||||
it('transforms it to a custom (mutable) rule', () => {
|
||||
it('transforms it to a custom (mutable) rule', async () => {
|
||||
const rule = createPrebuiltRule();
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -168,7 +178,7 @@ describe('duplicateRule', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('resets related integrations to an empty array', () => {
|
||||
it('resets related integrations to an empty array', async () => {
|
||||
const rule = createPrebuiltRule();
|
||||
rule.params.relatedIntegrations = [
|
||||
{
|
||||
|
@ -178,7 +188,9 @@ describe('duplicateRule', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -189,7 +201,7 @@ describe('duplicateRule', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('resets required fields to an empty array', () => {
|
||||
it('resets required fields to an empty array', async () => {
|
||||
const rule = createPrebuiltRule();
|
||||
rule.params.requiredFields = [
|
||||
{
|
||||
|
@ -199,7 +211,9 @@ describe('duplicateRule', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -210,10 +224,12 @@ describe('duplicateRule', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('resets setup guide to an empty string', () => {
|
||||
it('resets setup guide to an empty string', async () => {
|
||||
const rule = createPrebuiltRule();
|
||||
rule.params.setup = `## Config\n\nThe 'Audit Detailed File Share' audit policy must be configured...`;
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -232,9 +248,11 @@ describe('duplicateRule', () => {
|
|||
return rule;
|
||||
};
|
||||
|
||||
it('keeps it custom', () => {
|
||||
it('keeps it custom', async () => {
|
||||
const rule = createCustomRule();
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -245,7 +263,7 @@ describe('duplicateRule', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('copies related integrations as is', () => {
|
||||
it('copies related integrations as is', async () => {
|
||||
const rule = createCustomRule();
|
||||
rule.params.relatedIntegrations = [
|
||||
{
|
||||
|
@ -255,7 +273,9 @@ describe('duplicateRule', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -266,7 +286,7 @@ describe('duplicateRule', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('copies required fields as is', () => {
|
||||
it('copies required fields as is', async () => {
|
||||
const rule = createCustomRule();
|
||||
rule.params.requiredFields = [
|
||||
{
|
||||
|
@ -276,7 +296,9 @@ describe('duplicateRule', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -287,10 +309,12 @@ describe('duplicateRule', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('copies setup guide as is', () => {
|
||||
it('copies setup guide as is', async () => {
|
||||
const rule = createCustomRule();
|
||||
rule.params.setup = `## Config\n\nThe 'Audit Detailed File Share' audit policy must be configured...`;
|
||||
const result = duplicateRule(rule);
|
||||
const result = await duplicateRule({
|
||||
rule,
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
|
|
|
@ -9,7 +9,6 @@ import uuid from 'uuid';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ruleTypeMappings } from '@kbn/securitysolution-rules';
|
||||
import type { SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
|
||||
import { SERVER_APP_ID } from '../../../../../../common/constants';
|
||||
import type { InternalRuleCreate, RuleParams } from '../../../rule_schema';
|
||||
|
||||
|
@ -20,7 +19,11 @@ const DUPLICATE_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const duplicateRule = (rule: SanitizedRule<RuleParams>): InternalRuleCreate => {
|
||||
interface DuplicateRuleParams {
|
||||
rule: SanitizedRule<RuleParams>;
|
||||
}
|
||||
|
||||
export const duplicateRule = async ({ rule }: DuplicateRuleParams): Promise<InternalRuleCreate> => {
|
||||
// Generate a new static ruleId
|
||||
const ruleId = uuid.v4();
|
||||
|
||||
|
@ -43,6 +46,7 @@ export const duplicateRule = (rule: SanitizedRule<RuleParams>): InternalRuleCrea
|
|||
relatedIntegrations,
|
||||
requiredFields,
|
||||
setup,
|
||||
exceptionsList: [],
|
||||
},
|
||||
schedule: rule.schedule,
|
||||
enabled: false,
|
||||
|
|
|
@ -310,7 +310,11 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
await createRule(supertest, log, ruleToDuplicate);
|
||||
|
||||
const { body } = await postBulkAction()
|
||||
.send({ query: '', action: BulkActionType.duplicate })
|
||||
.send({
|
||||
query: '',
|
||||
action: BulkActionType.duplicate,
|
||||
duplicate: { include_exceptions: false },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 });
|
||||
|
@ -352,7 +356,11 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
);
|
||||
|
||||
const { body } = await postBulkAction()
|
||||
.send({ query: '', action: BulkActionType.duplicate })
|
||||
.send({
|
||||
query: '',
|
||||
action: BulkActionType.duplicate,
|
||||
duplicate: { include_exceptions: false },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 });
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue