mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution][Exceptions] - Add exception list duplication options with and without expired items (#154991)
## Summary Adds the following: - Add the option to duplicate from the shared exception list management actions dropdowns - User can select to include exception items with expired TTL - User can select to not include exception items with expired TTL - Cypress tests added for both options
This commit is contained in:
parent
fdc23f570e
commit
11155329cc
67 changed files with 2303 additions and 332 deletions
|
@ -1876,6 +1876,27 @@ Object {
|
|||
data-test-subj="RightSideMenuItemsMenuActionsActionItem2"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__itemLayout"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="copy"
|
||||
/>
|
||||
<span
|
||||
class="euiContextMenuItem__text"
|
||||
>
|
||||
Duplicate exception list
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="euiContextMenuItem euiContextMenuItem--small euiContextMenuItem-isDisabled"
|
||||
data-test-subj="RightSideMenuItemsMenuActionsActionItem3"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__itemLayout"
|
||||
|
|
|
@ -32,6 +32,7 @@ interface ExceptionListHeaderComponentProps {
|
|||
onDeleteList: () => void;
|
||||
onManageRules: () => void;
|
||||
onExportList: () => void;
|
||||
onDuplicateList: () => void;
|
||||
}
|
||||
|
||||
export interface BackOptions {
|
||||
|
@ -54,6 +55,7 @@ const ExceptionListHeaderComponent: FC<ExceptionListHeaderComponentProps> = ({
|
|||
onDeleteList,
|
||||
onManageRules,
|
||||
onExportList,
|
||||
onDuplicateList,
|
||||
}) => {
|
||||
const { isModalVisible, listDetails, onEdit, onSave, onCancel } = useExceptionListHeader({
|
||||
name,
|
||||
|
@ -100,6 +102,7 @@ const ExceptionListHeaderComponent: FC<ExceptionListHeaderComponentProps> = ({
|
|||
onDeleteList={onDeleteList}
|
||||
onManageRules={onManageRules}
|
||||
onExportList={onExportList}
|
||||
onDuplicateList={onDuplicateList}
|
||||
/>,
|
||||
]}
|
||||
breadcrumbs={[
|
||||
|
|
|
@ -17,6 +17,7 @@ const onExportList = jest.fn();
|
|||
const onDeleteList = jest.fn();
|
||||
const onManageRules = jest.fn();
|
||||
const onNavigate = jest.fn();
|
||||
const onDuplicateList = jest.fn();
|
||||
jest.mock('./use_list_header');
|
||||
|
||||
describe('ExceptionListHeader', () => {
|
||||
|
@ -40,6 +41,7 @@ describe('ExceptionListHeader', () => {
|
|||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onManageRules={onManageRules}
|
||||
onDuplicateList={onDuplicateList}
|
||||
backOptions={{ pageId: '', path: '', onNavigate }}
|
||||
/>
|
||||
);
|
||||
|
@ -70,6 +72,7 @@ describe('ExceptionListHeader', () => {
|
|||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onManageRules={onManageRules}
|
||||
onDuplicateList={onDuplicateList}
|
||||
backOptions={{ pageId: '', path: '', onNavigate }}
|
||||
/>
|
||||
);
|
||||
|
@ -79,6 +82,7 @@ describe('ExceptionListHeader', () => {
|
|||
|
||||
expect(wrapper.queryByTestId('RightSideMenuItemsMenuActionsActionItem1')).toBeEnabled();
|
||||
expect(wrapper.queryByTestId('RightSideMenuItemsMenuActionsActionItem2')).toBeDisabled();
|
||||
expect(wrapper.queryByTestId('RightSideMenuItemsMenuActionsActionItem3')).toBeDisabled();
|
||||
expect(wrapper.queryByTestId('EditTitleIcon')).not.toBeInTheDocument();
|
||||
});
|
||||
it('should render the List Header with name, default description and actions', () => {
|
||||
|
@ -93,6 +97,7 @@ describe('ExceptionListHeader', () => {
|
|||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onManageRules={onManageRules}
|
||||
onDuplicateList={onDuplicateList}
|
||||
backOptions={{ pageId: '', path: '', onNavigate }}
|
||||
/>
|
||||
);
|
||||
|
@ -123,6 +128,7 @@ describe('ExceptionListHeader', () => {
|
|||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onManageRules={onManageRules}
|
||||
onDuplicateList={onDuplicateList}
|
||||
backOptions={{ pageId: '', path: '', onNavigate }}
|
||||
/>
|
||||
);
|
||||
|
@ -148,6 +154,7 @@ describe('ExceptionListHeader', () => {
|
|||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onManageRules={onManageRules}
|
||||
onDuplicateList={onDuplicateList}
|
||||
backOptions={{ pageId: '', path: 'test-path', onNavigate }}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -528,6 +528,26 @@ Object {
|
|||
class="euiContextMenuItem euiContextMenuItem--small"
|
||||
data-test-subj="MenuActionsActionItem2"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__itemLayout"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="copy"
|
||||
/>
|
||||
<span
|
||||
class="euiContextMenuItem__text"
|
||||
>
|
||||
Duplicate exception list
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="euiContextMenuItem euiContextMenuItem--small"
|
||||
data-test-subj="MenuActionsActionItem3"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__itemLayout"
|
||||
|
@ -671,7 +691,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`MenuItems should render delete action disabled 1`] = `
|
||||
exports[`MenuItems should render delete action disabled when "canUserEditList" is "false" 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
|
@ -809,6 +829,27 @@ Object {
|
|||
data-test-subj="MenuActionsActionItem2"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__itemLayout"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="copy"
|
||||
/>
|
||||
<span
|
||||
class="euiContextMenuItem__text"
|
||||
>
|
||||
Duplicate exception list
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="euiContextMenuItem euiContextMenuItem--small euiContextMenuItem-isDisabled"
|
||||
data-test-subj="MenuActionsActionItem3"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiContextMenu__itemLayout"
|
||||
|
|
|
@ -21,6 +21,7 @@ interface MenuItemsProps {
|
|||
onDeleteList: () => void;
|
||||
onManageRules: () => void;
|
||||
onExportList: () => void;
|
||||
onDuplicateList: () => void;
|
||||
}
|
||||
|
||||
const MenuItemsComponent: FC<MenuItemsProps> = ({
|
||||
|
@ -32,6 +33,7 @@ const MenuItemsComponent: FC<MenuItemsProps> = ({
|
|||
onDeleteList,
|
||||
onManageRules,
|
||||
onExportList,
|
||||
onDuplicateList,
|
||||
}) => {
|
||||
const referencedLinks = useMemo(
|
||||
() =>
|
||||
|
@ -100,6 +102,15 @@ const MenuItemsComponent: FC<MenuItemsProps> = ({
|
|||
},
|
||||
{
|
||||
key: '2',
|
||||
icon: 'copy',
|
||||
label: i18n.EXCEPTION_LIST_HEADER_DUPLICATE_ACTION,
|
||||
onClick: () => {
|
||||
if (typeof onDuplicateList === 'function') onDuplicateList();
|
||||
},
|
||||
disabled: !canUserEditList,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
icon: 'trash',
|
||||
label: i18n.EXCEPTION_LIST_HEADER_DELETE_ACTION,
|
||||
onClick: () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import { securityLinkAnchorComponentMock } from '../../mocks/security_link_compo
|
|||
const onExportList = jest.fn();
|
||||
const onDeleteList = jest.fn();
|
||||
const onManageRules = jest.fn();
|
||||
const onDuplicateList = jest.fn();
|
||||
describe('MenuItems', () => {
|
||||
it('should render linkedRules, manageRules and menuActions', () => {
|
||||
const wrapper = render(
|
||||
|
@ -24,6 +25,7 @@ describe('MenuItems', () => {
|
|||
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
|
||||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onDuplicateList={onDuplicateList}
|
||||
onManageRules={onManageRules}
|
||||
/>
|
||||
);
|
||||
|
@ -40,6 +42,7 @@ describe('MenuItems', () => {
|
|||
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
|
||||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onDuplicateList={onDuplicateList}
|
||||
onManageRules={onManageRules}
|
||||
/>
|
||||
);
|
||||
|
@ -55,6 +58,7 @@ describe('MenuItems', () => {
|
|||
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
|
||||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onDuplicateList={onDuplicateList}
|
||||
onManageRules={onManageRules}
|
||||
/>
|
||||
);
|
||||
|
@ -62,8 +66,9 @@ describe('MenuItems', () => {
|
|||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.getByTestId('MenuActionsActionItem1')).toBeEnabled();
|
||||
expect(wrapper.getByTestId('MenuActionsActionItem2')).toBeEnabled();
|
||||
expect(wrapper.getByTestId('MenuActionsActionItem3')).toBeEnabled();
|
||||
});
|
||||
it('should render delete action disabled', () => {
|
||||
it('should render delete action disabled when "canUserEditList" is "false"', () => {
|
||||
const wrapper = render(
|
||||
<MenuItems
|
||||
isReadonly={false}
|
||||
|
@ -72,12 +77,15 @@ describe('MenuItems', () => {
|
|||
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
|
||||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onDuplicateList={onDuplicateList}
|
||||
onManageRules={onManageRules}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(wrapper.getByTestId('MenuActionsButtonIcon'));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.getByTestId('MenuActionsActionItem1')).toBeEnabled();
|
||||
expect(wrapper.getByTestId('MenuActionsActionItem2')).toBeDisabled();
|
||||
expect(wrapper.getByTestId('MenuActionsActionItem3')).toBeDisabled();
|
||||
});
|
||||
it('should not render Manage rules', () => {
|
||||
const wrapper = render(
|
||||
|
@ -88,6 +96,7 @@ describe('MenuItems', () => {
|
|||
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
|
||||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onDuplicateList={onDuplicateList}
|
||||
onManageRules={onManageRules}
|
||||
/>
|
||||
);
|
||||
|
@ -102,6 +111,7 @@ describe('MenuItems', () => {
|
|||
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
|
||||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onDuplicateList={onDuplicateList}
|
||||
onManageRules={onManageRules}
|
||||
/>
|
||||
);
|
||||
|
@ -116,6 +126,7 @@ describe('MenuItems', () => {
|
|||
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
|
||||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onDuplicateList={onDuplicateList}
|
||||
onManageRules={onManageRules}
|
||||
/>
|
||||
);
|
||||
|
@ -132,12 +143,31 @@ describe('MenuItems', () => {
|
|||
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
|
||||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onDuplicateList={onDuplicateList}
|
||||
onManageRules={onManageRules}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(wrapper.getByTestId('MenuActionsButtonIcon'));
|
||||
fireEvent.click(wrapper.getByTestId('MenuActionsActionItem3'));
|
||||
|
||||
expect(onDeleteList).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onDuplicateList', () => {
|
||||
const wrapper = render(
|
||||
<MenuItems
|
||||
isReadonly={false}
|
||||
linkedRules={rules}
|
||||
securityLinkAnchorComponent={securityLinkAnchorComponentMock}
|
||||
onExportList={onExportList}
|
||||
onDeleteList={onDeleteList}
|
||||
onDuplicateList={onDuplicateList}
|
||||
onManageRules={onManageRules}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(wrapper.getByTestId('MenuActionsButtonIcon'));
|
||||
fireEvent.click(wrapper.getByTestId('MenuActionsActionItem2'));
|
||||
|
||||
expect(onDeleteList).toHaveBeenCalled();
|
||||
expect(onDuplicateList).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -67,6 +67,12 @@ export const EXCEPTION_LIST_HEADER_DELETE_ACTION = i18n.translate(
|
|||
defaultMessage: 'Delete exception list',
|
||||
}
|
||||
);
|
||||
export const EXCEPTION_LIST_HEADER_DUPLICATE_ACTION = i18n.translate(
|
||||
'exceptionList-components.exception_list_header_duplicate_action',
|
||||
{
|
||||
defaultMessage: 'Duplicate exception list',
|
||||
}
|
||||
);
|
||||
export const EXCEPTION_LIST_HEADER_LINK_RULES_BUTTON = i18n.translate(
|
||||
'exceptionList-components.exception_list_header_link_rules_button',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { LIST_ID, NAMESPACE_TYPE } from '../../constants/index.mock';
|
||||
|
||||
import { DuplicateExceptionListQuerySchema } from '.';
|
||||
|
||||
export const getDuplicateExceptionListQuerySchemaMock = (): DuplicateExceptionListQuerySchema => ({
|
||||
list_id: LIST_ID,
|
||||
namespace_type: NAMESPACE_TYPE,
|
||||
include_expired_exceptions: 'true',
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
|
||||
import { DuplicateExceptionListQuerySchema, duplicateExceptionListQuerySchema } from '.';
|
||||
import { getDuplicateExceptionListQuerySchemaMock } from './index.mock';
|
||||
|
||||
describe('duplicate_exceptionList_query_schema', () => {
|
||||
test('it should validate a typical lists request', () => {
|
||||
const payload = getDuplicateExceptionListQuerySchemaMock();
|
||||
const decoded = duplicateExceptionListQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should default namespace_type to "single" if an undefined given for namespacetype', () => {
|
||||
const payload = getDuplicateExceptionListQuerySchemaMock();
|
||||
delete payload.namespace_type;
|
||||
const decoded = duplicateExceptionListQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(message.schema).toEqual({
|
||||
include_expired_exceptions: 'true',
|
||||
list_id: 'some-list-id',
|
||||
namespace_type: 'single',
|
||||
});
|
||||
});
|
||||
|
||||
test('it should NOT accept an undefined for an list_id', () => {
|
||||
const payload = getDuplicateExceptionListQuerySchemaMock();
|
||||
// @ts-expect-error
|
||||
delete payload.list_id;
|
||||
const decoded = duplicateExceptionListQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "undefined" supplied to "list_id"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not allow an extra key to be sent in', () => {
|
||||
const payload: DuplicateExceptionListQuerySchema & {
|
||||
extraKey?: string;
|
||||
} = getDuplicateExceptionListQuerySchemaMock();
|
||||
payload.extraKey = 'some new value';
|
||||
const decoded = duplicateExceptionListQuerySchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { NamespaceType } from '../../common';
|
||||
|
||||
import { includeExpiredExceptionsOrUndefined } from '../../common/include_expired_exceptions';
|
||||
import { list_id } from '../../common/list_id';
|
||||
import { namespace_type } from '../../common/namespace_type';
|
||||
|
||||
export const duplicateExceptionListQuerySchema = t.exact(
|
||||
t.type({
|
||||
list_id,
|
||||
namespace_type,
|
||||
include_expired_exceptions: includeExpiredExceptionsOrUndefined,
|
||||
})
|
||||
);
|
||||
|
||||
export type DuplicateExceptionListQuerySchema = t.OutputOf<
|
||||
typeof duplicateExceptionListQuerySchema
|
||||
>;
|
||||
|
||||
// This type is used after a decode since some things are defaults after a decode.
|
||||
export type DuplicateExceptionListQuerySchemaDecoded = Omit<
|
||||
t.TypeOf<typeof duplicateExceptionListQuerySchema>,
|
||||
'namespace_type'
|
||||
> & {
|
||||
namespace_type: NamespaceType;
|
||||
};
|
|
@ -16,6 +16,7 @@ export * from './delete_exception_list_schema';
|
|||
export * from './delete_exception_list_item_schema';
|
||||
export * from './delete_list_item_schema';
|
||||
export * from './delete_list_schema';
|
||||
export * from './duplicate_exception_list_query_schema';
|
||||
export * from './export_exception_list_query_schema';
|
||||
export * from './export_list_item_query_schema';
|
||||
export * from './find_endpoint_list_item_schema';
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
import type { NotificationsStart } from '@kbn/core-notifications-browser';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { NamespaceType } from '../common/default_namespace';
|
||||
import { ExceptionListType, ExceptionListTypeEnum } from '../common/exception_list';
|
||||
|
@ -20,13 +22,22 @@ import { UpdateExceptionListSchema } from '../request/update_exception_list_sche
|
|||
import { ExceptionListItemSchema } from '../response/exception_list_item_schema';
|
||||
import { ExceptionListSchema } from '../response/exception_list_schema';
|
||||
|
||||
// TODO: Replace these with kbn packaged versions once we have those available to us
|
||||
// These originally came from this location below before moving them to this hacked "any" types:
|
||||
// import { HttpStart, NotificationsStart } from '../../../../../src/core/public';
|
||||
interface HttpStart {
|
||||
fetch: <T>(...args: any) => any;
|
||||
interface BaseParams {
|
||||
http: HttpStart;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface DuplicateExceptionListProps extends BaseParams {
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
includeExpiredExceptions: boolean;
|
||||
}
|
||||
|
||||
export interface ApiListDuplicateProps
|
||||
extends Omit<DuplicateExceptionListProps, 'http' | 'signal'> {
|
||||
onError: (err: Error) => void;
|
||||
onSuccess: (newList: ExceptionListSchema) => void;
|
||||
}
|
||||
type NotificationsStart = any;
|
||||
|
||||
export interface ExceptionListFilter {
|
||||
name?: string | null;
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
"@kbn/securitysolution-io-ts-types",
|
||||
"@kbn/securitysolution-io-ts-utils",
|
||||
"@kbn/securitysolution-list-constants",
|
||||
"@kbn/es-query"
|
||||
"@kbn/es-query",
|
||||
"@kbn/core-http-browser",
|
||||
"@kbn/core-notifications-browser"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
GetExceptionFilterFromExceptionListIdsProps,
|
||||
GetExceptionFilterFromExceptionsProps,
|
||||
ExceptionFilterResponse,
|
||||
DuplicateExceptionListProps,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import {
|
||||
|
@ -617,3 +618,31 @@ export const getExceptionFilterFromExceptions = async ({
|
|||
}),
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Duplicate an ExceptionList and its items by providing a ExceptionList list_id
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param includeExpiredExceptions boolean for including exception items with expired TTL
|
||||
* @param listId ExceptionList LIST_ID (not id)
|
||||
* @param namespaceType ExceptionList namespace_type
|
||||
* @param signal to cancel request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const duplicateExceptionList = async ({
|
||||
http,
|
||||
includeExpiredExceptions,
|
||||
listId,
|
||||
namespaceType,
|
||||
signal,
|
||||
}: DuplicateExceptionListProps): Promise<ExceptionListSchema> =>
|
||||
http.fetch<ExceptionListSchema>(`${EXCEPTION_LIST_URL}/_duplicate`, {
|
||||
method: 'POST',
|
||||
query: {
|
||||
list_id: listId,
|
||||
namespace_type: namespaceType,
|
||||
include_expired_exceptions: includeExpiredExceptions,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
|
|
@ -17,17 +17,12 @@ import type {
|
|||
ApiListExportProps,
|
||||
ApiCallGetExceptionFilterFromIdsMemoProps,
|
||||
ApiCallGetExceptionFilterFromExceptionsMemoProps,
|
||||
ApiListDuplicateProps,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import * as Api from '@kbn/securitysolution-list-api';
|
||||
|
||||
// TODO: Replace these with kbn packaged versions once we have those available to us
|
||||
// These originally came from this location below before moving them to this hacked "any" types:
|
||||
// import { HttpStart, NotificationsStart } from '../../../../../src/core/public';
|
||||
interface HttpStart {
|
||||
fetch: <T>(...args: any) => any;
|
||||
}
|
||||
|
||||
import { getIdsAndNamespaces } from '@kbn/securitysolution-list-utils';
|
||||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
|
||||
import { transformInput, transformNewItemOutput, transformOutput } from '../transforms';
|
||||
|
||||
export interface ExceptionsApi {
|
||||
|
@ -39,6 +34,7 @@ export interface ExceptionsApi {
|
|||
}) => Promise<ExceptionListItemSchema>;
|
||||
deleteExceptionItem: (arg: ApiCallMemoProps) => Promise<void>;
|
||||
deleteExceptionList: (arg: ApiCallMemoProps) => Promise<void>;
|
||||
duplicateExceptionList: (arg: ApiListDuplicateProps) => Promise<void>;
|
||||
getExceptionItem: (
|
||||
arg: ApiCallMemoProps & { onSuccess: (arg: ExceptionListItemSchema) => void }
|
||||
) => Promise<void>;
|
||||
|
@ -110,6 +106,28 @@ export const useApi = (http: HttpStart): ExceptionsApi => {
|
|||
onError(error);
|
||||
}
|
||||
},
|
||||
async duplicateExceptionList({
|
||||
includeExpiredExceptions,
|
||||
listId,
|
||||
namespaceType,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: ApiListDuplicateProps): Promise<void> {
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
try {
|
||||
const newList = await Api.duplicateExceptionList({
|
||||
http,
|
||||
includeExpiredExceptions,
|
||||
listId,
|
||||
namespaceType,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
onSuccess(newList);
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
},
|
||||
async exportExceptionList({
|
||||
id,
|
||||
includeExpiredExceptions,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"@kbn/securitysolution-list-utils",
|
||||
"@kbn/securitysolution-utils",
|
||||
"@kbn/core-http-browser-mocks",
|
||||
"@kbn/core-http-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
addExceptionListItem,
|
||||
deleteExceptionListById,
|
||||
deleteExceptionListItemById,
|
||||
duplicateExceptionList,
|
||||
exportExceptionList,
|
||||
fetchExceptionListById,
|
||||
fetchExceptionListItemById,
|
||||
|
@ -728,4 +729,30 @@ describe('Exceptions Lists API', () => {
|
|||
expect(exceptionResponse).toEqual(blob);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#duplicateExceptionList', () => {
|
||||
beforeEach(() => {
|
||||
httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock());
|
||||
});
|
||||
|
||||
test('it invokes "duplicateExceptionList" with expected url and body values', async () => {
|
||||
await duplicateExceptionList({
|
||||
http: httpMock,
|
||||
includeExpiredExceptions: false,
|
||||
listId: 'my_list',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/_duplicate', {
|
||||
method: 'POST',
|
||||
query: {
|
||||
include_expired_exceptions: false,
|
||||
list_id: 'my_list',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
|||
AddExceptionListItemProps,
|
||||
ApiCallByIdProps,
|
||||
ApiCallByListIdProps,
|
||||
DuplicateExceptionListProps,
|
||||
UpdateExceptionListItemProps,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
|
@ -462,4 +463,61 @@ describe('useApi', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicateExceptionList', () => {
|
||||
test('it invokes "onSuccess" when duplication does not throw', async () => {
|
||||
const onSuccessMock = jest.fn();
|
||||
const spyOnDuplicateExceptionList = jest
|
||||
.spyOn(api, 'duplicateExceptionList')
|
||||
.mockResolvedValue(getExceptionListSchemaMock());
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
|
||||
useApi(mockKibanaHttpService)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
await result.current.duplicateExceptionList({
|
||||
includeExpiredExceptions: false,
|
||||
listId: 'my_list',
|
||||
namespaceType: 'single',
|
||||
onError: jest.fn(),
|
||||
onSuccess: onSuccessMock,
|
||||
});
|
||||
|
||||
const expected: DuplicateExceptionListProps = {
|
||||
http: mockKibanaHttpService,
|
||||
includeExpiredExceptions: false,
|
||||
listId: 'my_list',
|
||||
namespaceType: 'single',
|
||||
signal: new AbortController().signal,
|
||||
};
|
||||
|
||||
expect(spyOnDuplicateExceptionList).toHaveBeenCalledWith(expected);
|
||||
expect(onSuccessMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('invokes "onError" callback if "duplicateExceptionList" fails', async () => {
|
||||
const mockError = new Error('failed to duplicate item');
|
||||
jest.spyOn(api, 'duplicateExceptionList').mockRejectedValue(mockError);
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
|
||||
useApi(mockKibanaHttpService)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
await result.current.duplicateExceptionList({
|
||||
includeExpiredExceptions: false,
|
||||
listId: 'my_list',
|
||||
namespaceType: 'single',
|
||||
onError: onErrorMock,
|
||||
onSuccess: jest.fn(),
|
||||
});
|
||||
|
||||
expect(onErrorMock).toHaveBeenCalledWith(mockError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import {
|
||||
DuplicateExceptionListQuerySchemaDecoded,
|
||||
duplicateExceptionListQuerySchema,
|
||||
exceptionListSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { validate } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
|
||||
|
||||
import type { ListsPluginRouter } from '../types';
|
||||
|
||||
import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils';
|
||||
|
||||
export const duplicateExceptionsRoute = (router: ListsPluginRouter): void => {
|
||||
router.post(
|
||||
{
|
||||
options: {
|
||||
tags: ['access:lists-all'],
|
||||
},
|
||||
path: `${EXCEPTION_LIST_URL}/_duplicate`,
|
||||
validate: {
|
||||
query: buildRouteValidation<
|
||||
typeof duplicateExceptionListQuerySchema,
|
||||
DuplicateExceptionListQuerySchemaDecoded
|
||||
>(duplicateExceptionListQuerySchema),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const siemResponse = buildSiemResponse(response);
|
||||
|
||||
try {
|
||||
const {
|
||||
list_id: listId,
|
||||
namespace_type: namespaceType,
|
||||
include_expired_exceptions: includeExpiredExceptionsString,
|
||||
} = request.query;
|
||||
|
||||
const exceptionListsClient = await getExceptionListClient(context);
|
||||
|
||||
// fetch list container
|
||||
const listToDuplicate = await exceptionListsClient.getExceptionList({
|
||||
id: undefined,
|
||||
listId,
|
||||
namespaceType,
|
||||
});
|
||||
|
||||
if (listToDuplicate == null) {
|
||||
return siemResponse.error({
|
||||
body: `exception list id: "${listId}" does not exist`,
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
// Defaults to including expired exceptions if query param is not present
|
||||
const includeExpiredExceptions =
|
||||
includeExpiredExceptionsString !== undefined
|
||||
? includeExpiredExceptionsString === 'true'
|
||||
: true;
|
||||
const duplicatedList = await exceptionListsClient.duplicateExceptionListAndItems({
|
||||
includeExpiredExceptions,
|
||||
list: listToDuplicate,
|
||||
namespaceType,
|
||||
});
|
||||
|
||||
if (duplicatedList == null) {
|
||||
return siemResponse.error({
|
||||
body: `unable to duplicate exception list with list_id: ${listId} - action not allowed`,
|
||||
statusCode: 405,
|
||||
});
|
||||
}
|
||||
|
||||
const [validated, errors] = validate(duplicatedList, exceptionListSchema);
|
||||
if (errors != null) {
|
||||
return siemResponse.error({ body: errors, statusCode: 500 });
|
||||
} else {
|
||||
return response.ok({ body: validated ?? {} });
|
||||
}
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
return siemResponse.error({
|
||||
body: error.message,
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -18,6 +18,7 @@ export * from './delete_exception_list_item_route';
|
|||
export * from './delete_list_index_route';
|
||||
export * from './delete_list_item_route';
|
||||
export * from './delete_list_route';
|
||||
export * from './duplicate_exception_list_route';
|
||||
export * from './export_exception_list_route';
|
||||
export * from './export_list_item_route';
|
||||
export * from './find_endpoint_list_item_route';
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
deleteListIndexRoute,
|
||||
deleteListItemRoute,
|
||||
deleteListRoute,
|
||||
duplicateExceptionsRoute,
|
||||
exportExceptionsRoute,
|
||||
exportListItemRoute,
|
||||
findEndpointListItemRoute,
|
||||
|
@ -87,6 +88,7 @@ export const initRoutes = (router: ListsPluginRouter, config: ConfigType): void
|
|||
updateExceptionListRoute(router);
|
||||
deleteExceptionListRoute(router);
|
||||
findExceptionListRoute(router);
|
||||
duplicateExceptionsRoute(router);
|
||||
|
||||
// exception list items
|
||||
createExceptionListItemRoute(router);
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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 { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import {
|
||||
getDetectionsExceptionListSchemaMock,
|
||||
getTrustedAppsListSchemaMock,
|
||||
} from '../../../common/schemas/response/exception_list_schema.mock';
|
||||
import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock';
|
||||
|
||||
import { findExceptionListsItemPointInTimeFinder } from './find_exception_list_items_point_in_time_finder';
|
||||
import { duplicateExceptionListAndItems } from './duplicate_exception_list';
|
||||
import { getExceptionList } from './get_exception_list';
|
||||
import { createExceptionList } from './create_exception_list';
|
||||
|
||||
jest.mock('./get_exception_list');
|
||||
jest.mock('./create_exception_list');
|
||||
jest.mock('./bulk_create_exception_list_items');
|
||||
jest.mock('./find_exception_list_items_point_in_time_finder');
|
||||
|
||||
const mockCurrentTime = new Date('2023-02-01T10:20:30Z');
|
||||
|
||||
describe('duplicateExceptionListAndItems', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(mockCurrentTime);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('should return null exception list is not of type "detection" or "rule_default"', async () => {
|
||||
(getExceptionList as jest.Mock).mockResolvedValue(getTrustedAppsListSchemaMock());
|
||||
|
||||
const result = await duplicateExceptionListAndItems({
|
||||
includeExpiredExceptions: true,
|
||||
list: getTrustedAppsListSchemaMock(),
|
||||
namespaceType: 'single',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
user: 'test-user',
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('should duplicate a list with expired exceptions', async () => {
|
||||
(getExceptionList as jest.Mock).mockResolvedValue(getDetectionsExceptionListSchemaMock());
|
||||
(createExceptionList as jest.Mock).mockResolvedValue({
|
||||
...getDetectionsExceptionListSchemaMock(),
|
||||
list_id: 'exception_list_id_dupe',
|
||||
name: 'Test [Duplicate]',
|
||||
});
|
||||
(findExceptionListsItemPointInTimeFinder as jest.Mock).mockImplementationOnce(
|
||||
({ executeFunctionOnStream }) => {
|
||||
executeFunctionOnStream({ data: [getExceptionListItemSchemaMock()] });
|
||||
}
|
||||
);
|
||||
|
||||
await duplicateExceptionListAndItems({
|
||||
includeExpiredExceptions: true,
|
||||
list: getDetectionsExceptionListSchemaMock(),
|
||||
namespaceType: 'single',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
user: 'test-user',
|
||||
});
|
||||
|
||||
expect(findExceptionListsItemPointInTimeFinder).toHaveBeenCalledWith({
|
||||
executeFunctionOnStream: expect.any(Function),
|
||||
filter: [],
|
||||
listId: ['exception_list_id'],
|
||||
maxSize: 10000,
|
||||
namespaceType: ['single'],
|
||||
perPage: undefined,
|
||||
savedObjectsClient: expect.any(Object),
|
||||
sortField: undefined,
|
||||
sortOrder: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('should duplicate a list without expired exceptions', async () => {
|
||||
(getExceptionList as jest.Mock).mockResolvedValue(getDetectionsExceptionListSchemaMock());
|
||||
(createExceptionList as jest.Mock).mockResolvedValue({
|
||||
...getDetectionsExceptionListSchemaMock(),
|
||||
list_id: 'exception_list_id_dupe',
|
||||
name: 'Test [Duplicate]',
|
||||
});
|
||||
(findExceptionListsItemPointInTimeFinder as jest.Mock).mockImplementationOnce(
|
||||
({ executeFunctionOnStream }) => {
|
||||
executeFunctionOnStream({ data: [getExceptionListItemSchemaMock()] });
|
||||
}
|
||||
);
|
||||
|
||||
await duplicateExceptionListAndItems({
|
||||
includeExpiredExceptions: false,
|
||||
list: getDetectionsExceptionListSchemaMock(),
|
||||
namespaceType: 'single',
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
user: 'test-user',
|
||||
});
|
||||
|
||||
expect(findExceptionListsItemPointInTimeFinder).toHaveBeenCalledWith({
|
||||
executeFunctionOnStream: expect.any(Function),
|
||||
filter: [
|
||||
'(exception-list.attributes.expire_time > "2023-02-01T10:20:30.000Z" OR NOT exception-list.attributes.expire_time: *)',
|
||||
],
|
||||
listId: ['exception_list_id'],
|
||||
maxSize: 10000,
|
||||
namespaceType: ['single'],
|
||||
perPage: undefined,
|
||||
savedObjectsClient: expect.any(Object),
|
||||
sortField: undefined,
|
||||
sortOrder: undefined,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -12,13 +12,12 @@ import {
|
|||
ExceptionListSchema,
|
||||
ExceptionListTypeEnum,
|
||||
FoundExceptionListItemSchema,
|
||||
ListId,
|
||||
NamespaceType,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { getSavedObjectType } from '@kbn/securitysolution-list-utils';
|
||||
|
||||
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 = [
|
||||
|
@ -26,48 +25,38 @@ const LISTS_ABLE_TO_DUPLICATE = [
|
|||
ExceptionListTypeEnum.RULE_DEFAULT.toString(),
|
||||
];
|
||||
|
||||
interface CreateExceptionListOptions {
|
||||
listId: ListId;
|
||||
interface DuplicateExceptionListOptions {
|
||||
list: ExceptionListSchema;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
namespaceType: NamespaceType;
|
||||
user: string;
|
||||
includeExpiredExceptions: boolean;
|
||||
}
|
||||
|
||||
export const duplicateExceptionListAndItems = async ({
|
||||
listId,
|
||||
includeExpiredExceptions,
|
||||
list,
|
||||
savedObjectsClient,
|
||||
namespaceType,
|
||||
user,
|
||||
}: CreateExceptionListOptions): Promise<ExceptionListSchema> => {
|
||||
}: DuplicateExceptionListOptions): Promise<ExceptionListSchema | null> => {
|
||||
// Generate a new static listId
|
||||
const newListId = uuidv4();
|
||||
|
||||
// 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.`);
|
||||
if (!LISTS_ABLE_TO_DUPLICATE.includes(list.type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newlyCreatedList = await createExceptionList({
|
||||
description: listToDuplicate.description,
|
||||
immutable: listToDuplicate.immutable,
|
||||
description: list.description,
|
||||
immutable: list.immutable,
|
||||
listId: newListId,
|
||||
meta: listToDuplicate.meta,
|
||||
name: listToDuplicate.name,
|
||||
namespaceType: listToDuplicate.namespace_type,
|
||||
meta: list.meta,
|
||||
name: `${list.name} [Duplicate]`,
|
||||
namespaceType: list.namespace_type,
|
||||
savedObjectsClient,
|
||||
tags: listToDuplicate.tags,
|
||||
type: listToDuplicate.type,
|
||||
tags: list.tags,
|
||||
type: list.type,
|
||||
user,
|
||||
version: 1,
|
||||
});
|
||||
|
@ -96,10 +85,16 @@ export const duplicateExceptionListAndItems = async ({
|
|||
});
|
||||
itemsToBeDuplicated = [...itemsToBeDuplicated, ...transformedItems];
|
||||
};
|
||||
const savedObjectPrefix = getSavedObjectType({ namespaceType });
|
||||
const filter = includeExpiredExceptions
|
||||
? []
|
||||
: [
|
||||
`(${savedObjectPrefix}.attributes.expire_time > "${new Date().toISOString()}" OR NOT ${savedObjectPrefix}.attributes.expire_time: *)`,
|
||||
];
|
||||
await findExceptionListsItemPointInTimeFinder({
|
||||
executeFunctionOnStream,
|
||||
filter: [],
|
||||
listId: [listId],
|
||||
filter,
|
||||
listId: [list.list_id],
|
||||
maxSize: 10000,
|
||||
namespaceType: [namespaceType],
|
||||
perPage: undefined,
|
||||
|
|
|
@ -317,17 +317,20 @@ 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.list the "list" to be duplicated
|
||||
* @param options.namespaceType saved object namespace (single | agnostic)
|
||||
* @param options.includeExpiredExceptions include or exclude expired TTL exception items
|
||||
* @returns The exception list schema or null if it does not exist
|
||||
*/
|
||||
public duplicateExceptionListAndItems = async ({
|
||||
listId,
|
||||
list,
|
||||
namespaceType,
|
||||
includeExpiredExceptions,
|
||||
}: DuplicateExceptionListOptions): Promise<ExceptionListSchema | null> => {
|
||||
const { savedObjectsClient, user } = this;
|
||||
return duplicateExceptionListAndItems({
|
||||
listId,
|
||||
includeExpiredExceptions,
|
||||
list,
|
||||
namespaceType,
|
||||
savedObjectsClient,
|
||||
user,
|
||||
|
@ -1051,6 +1054,7 @@ export class ExceptionListClient {
|
|||
* @param options.listId the "list_id" of an exception list
|
||||
* @param options.id the "id" of an exception list
|
||||
* @param options.namespaceType saved object namespace (single | agnostic)
|
||||
* @param options.includeExpiredExceptions include or exclude expired TTL exception items
|
||||
* @returns the ndjson of the list and items to export or null if none exists
|
||||
*/
|
||||
public exportExceptionListAndItems = async ({
|
||||
|
|
|
@ -19,6 +19,7 @@ import type {
|
|||
EntriesArray,
|
||||
ExceptionListItemType,
|
||||
ExceptionListItemTypeOrUndefined,
|
||||
ExceptionListSchema,
|
||||
ExceptionListType,
|
||||
ExceptionListTypeOrUndefined,
|
||||
ExpireTimeOrUndefined,
|
||||
|
@ -295,10 +296,12 @@ export interface CreateEndpointListItemOptions {
|
|||
* {@link ExceptionListClient.duplicateExceptionListAndItems}
|
||||
*/
|
||||
export interface DuplicateExceptionListOptions {
|
||||
/** The single list id to do the search against */
|
||||
listId: ListId;
|
||||
/** The list to be duplicated */
|
||||
list: ExceptionListSchema;
|
||||
/** saved object namespace (single | agnostic) */
|
||||
namespaceType: NamespaceType;
|
||||
/** determines whether exception items with an expired TTL are included in duplication */
|
||||
includeExpiredExceptions: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -133,7 +133,10 @@ describe('Perform bulk action request schema', () => {
|
|||
const payload: PerformBulkActionRequestBody = {
|
||||
query: 'name: test',
|
||||
action: BulkActionType.duplicate,
|
||||
[BulkActionType.duplicate]: { include_exceptions: false },
|
||||
[BulkActionType.duplicate]: {
|
||||
include_exceptions: false,
|
||||
include_expired_exceptions: false,
|
||||
},
|
||||
};
|
||||
const message = retrieveValidationMessage(payload);
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
|
|
|
@ -136,6 +136,7 @@ export const BulkActionEditPayload = t.union([
|
|||
const bulkActionDuplicatePayload = t.exact(
|
||||
t.type({
|
||||
include_exceptions: t.boolean,
|
||||
include_expired_exceptions: t.boolean,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
|
||||
export enum DuplicateOptions {
|
||||
withExceptions = 'withExceptions',
|
||||
withExceptionsExcludeExpiredExceptions = 'withExceptionsExcludeExpiredExceptions',
|
||||
withoutExceptions = 'withoutExceptions',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 {
|
||||
waitForRulesTableToBeLoaded,
|
||||
goToTheRuleDetailsOf,
|
||||
selectNumberOfRules,
|
||||
duplicateSelectedRulesWithoutExceptions,
|
||||
expectManagementTableRules,
|
||||
duplicateSelectedRulesWithExceptions,
|
||||
duplicateSelectedRulesWithNonExpiredExceptions,
|
||||
} from '../../tasks/alerts_detection_rules';
|
||||
|
||||
import { goToExceptionsTab, viewExpiredExceptionItems } from '../../tasks/rule_details';
|
||||
import { login, visitWithoutDateRange } from '../../tasks/login';
|
||||
|
||||
import { SECURITY_DETECTIONS_RULES_URL } from '../../urls/navigation';
|
||||
import { createRule } from '../../tasks/api_calls/rules';
|
||||
import { cleanKibana, resetRulesTableState, deleteAlertsAndRules } from '../../tasks/common';
|
||||
|
||||
import { getNewRule } from '../../objects/rule';
|
||||
|
||||
import { esArchiverResetKibana } from '../../tasks/es_archiver';
|
||||
|
||||
import { createRuleExceptionItem } from '../../tasks/api_calls/exceptions';
|
||||
import { EXCEPTION_CARD_ITEM_NAME } from '../../screens/exceptions';
|
||||
import {
|
||||
assertExceptionItemsExists,
|
||||
assertNumberOfExceptionItemsExists,
|
||||
} from '../../tasks/exceptions';
|
||||
|
||||
const RULE_NAME = 'Custom rule for bulk actions';
|
||||
|
||||
const prePopulatedIndexPatterns = ['index-1-*', 'index-2-*'];
|
||||
const prePopulatedTags = ['test-default-tag-1', 'test-default-tag-2'];
|
||||
|
||||
const defaultRuleData = {
|
||||
index: prePopulatedIndexPatterns,
|
||||
tags: prePopulatedTags,
|
||||
};
|
||||
|
||||
const expiredDate = new Date(Date.now() - 1000000).toISOString();
|
||||
const futureDate = new Date(Date.now() + 1000000).toISOString();
|
||||
|
||||
const EXPIRED_EXCEPTION_ITEM_NAME = 'Sample exception item';
|
||||
|
||||
const NON_EXPIRED_EXCEPTION_ITEM_NAME = 'Sample exception item with future expiration';
|
||||
|
||||
describe('Detection rules, bulk duplicate', () => {
|
||||
before(() => {
|
||||
cleanKibana();
|
||||
login();
|
||||
});
|
||||
beforeEach(() => {
|
||||
// Make sure persisted rules table state is cleared
|
||||
resetRulesTableState();
|
||||
deleteAlertsAndRules();
|
||||
esArchiverResetKibana();
|
||||
createRule(getNewRule({ name: RULE_NAME, ...defaultRuleData, rule_id: '1' })).then(
|
||||
(response) => {
|
||||
createRuleExceptionItem(response.body.id, [
|
||||
{
|
||||
description: 'Exception item for rule default exception list',
|
||||
entries: [
|
||||
{
|
||||
field: 'user.name',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'some value',
|
||||
},
|
||||
],
|
||||
name: EXPIRED_EXCEPTION_ITEM_NAME,
|
||||
type: 'simple',
|
||||
expire_time: expiredDate,
|
||||
},
|
||||
{
|
||||
description: 'Exception item for rule default exception list',
|
||||
entries: [
|
||||
{
|
||||
field: 'user.name',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'some value',
|
||||
},
|
||||
],
|
||||
name: NON_EXPIRED_EXCEPTION_ITEM_NAME,
|
||||
type: 'simple',
|
||||
expire_time: futureDate,
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL);
|
||||
|
||||
waitForRulesTableToBeLoaded();
|
||||
});
|
||||
|
||||
it('Duplicates rules', () => {
|
||||
selectNumberOfRules(1);
|
||||
duplicateSelectedRulesWithoutExceptions();
|
||||
expectManagementTableRules([`${RULE_NAME} [Duplicate]`]);
|
||||
});
|
||||
|
||||
describe('With exceptions', () => {
|
||||
it('Duplicates rules with expired exceptions', () => {
|
||||
selectNumberOfRules(1);
|
||||
duplicateSelectedRulesWithExceptions();
|
||||
expectManagementTableRules([`${RULE_NAME} [Duplicate]`]);
|
||||
goToTheRuleDetailsOf(`${RULE_NAME} [Duplicate]`);
|
||||
goToExceptionsTab();
|
||||
assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [NON_EXPIRED_EXCEPTION_ITEM_NAME]);
|
||||
viewExpiredExceptionItems();
|
||||
assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [EXPIRED_EXCEPTION_ITEM_NAME]);
|
||||
});
|
||||
|
||||
it('Duplicates rules with exceptions, excluding expired exceptions', () => {
|
||||
selectNumberOfRules(1);
|
||||
duplicateSelectedRulesWithNonExpiredExceptions();
|
||||
expectManagementTableRules([`${RULE_NAME} [Duplicate]`]);
|
||||
goToTheRuleDetailsOf(`${RULE_NAME} [Duplicate]`);
|
||||
goToExceptionsTab();
|
||||
assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [NON_EXPIRED_EXCEPTION_ITEM_NAME]);
|
||||
viewExpiredExceptionItems();
|
||||
assertNumberOfExceptionItemsExists(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -58,12 +58,12 @@ import { INDICATOR_MATCH_ROW_RENDER, PROVIDER_BADGE } from '../../screens/timeli
|
|||
import { investigateFirstAlertInTimeline } from '../../tasks/alerts';
|
||||
import {
|
||||
duplicateFirstRule,
|
||||
duplicateSelectedRules,
|
||||
duplicateRuleFromMenu,
|
||||
goToRuleDetails,
|
||||
selectNumberOfRules,
|
||||
checkDuplicatedRule,
|
||||
expectNumberOfRules,
|
||||
duplicateSelectedRulesWithExceptions,
|
||||
} from '../../tasks/alerts_detection_rules';
|
||||
import { createRule } from '../../tasks/api_calls/rules';
|
||||
import { loadPrepackagedTimelineTemplates } from '../../tasks/api_calls/timelines';
|
||||
|
@ -544,7 +544,7 @@ describe('indicator match', () => {
|
|||
|
||||
it("Allows the rule to be duplicated from the table's bulk actions", () => {
|
||||
selectNumberOfRules(1);
|
||||
duplicateSelectedRules();
|
||||
duplicateSelectedRulesWithExceptions();
|
||||
checkDuplicatedRule();
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { login, visitWithoutDateRange, waitForPageWithoutDateRange } from '../..
|
|||
import { EXCEPTIONS_URL } from '../../../urls/navigation';
|
||||
import {
|
||||
deleteExceptionListWithRuleReferenceByListId,
|
||||
deleteExceptionListWithoutRuleReference,
|
||||
deleteExceptionListWithoutRuleReferenceByListId,
|
||||
exportExceptionList,
|
||||
searchForExceptionList,
|
||||
waitForExceptionsTableToBeLoaded,
|
||||
|
@ -153,7 +153,7 @@ describe('Exceptions Table', () => {
|
|||
// just checking number of lists shown
|
||||
cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3');
|
||||
|
||||
deleteExceptionListWithoutRuleReference();
|
||||
deleteExceptionListWithoutRuleReferenceByListId(getExceptionList1().list_id);
|
||||
|
||||
// Using cy.contains because we do not care about the exact text,
|
||||
// just checking number of lists shown
|
||||
|
|
|
@ -9,7 +9,10 @@ import { esArchiverResetKibana } from '../../../tasks/es_archiver';
|
|||
import { cleanKibana } from '../../../tasks/common';
|
||||
import { ROLES } from '../../../../common/test';
|
||||
import { getExceptionList } from '../../../objects/exception';
|
||||
import { EXCEPTIONS_TABLE_SHOWING_LISTS } from '../../../screens/exceptions';
|
||||
import {
|
||||
EXCEPTIONS_OVERFLOW_ACTIONS_BTN,
|
||||
EXCEPTIONS_TABLE_SHOWING_LISTS,
|
||||
} from '../../../screens/exceptions';
|
||||
import { createExceptionList, deleteExceptionList } from '../../../tasks/api_calls/exceptions';
|
||||
import {
|
||||
dismissCallOut,
|
||||
|
@ -56,4 +59,8 @@ describe('All exception lists - read only', () => {
|
|||
getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('Exception list actions should be disabled', () => {
|
||||
cy.get(EXCEPTIONS_OVERFLOW_ACTIONS_BTN).first().should('be.disabled');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ROLES } from '../../../../common/test';
|
||||
import { getExceptionList, expectedExportedExceptionList } from '../../../objects/exception';
|
||||
import { getNewRule } from '../../../objects/rule';
|
||||
|
||||
|
@ -14,8 +13,11 @@ import { login, visitWithoutDateRange, waitForPageWithoutDateRange } from '../..
|
|||
|
||||
import { EXCEPTIONS_URL } from '../../../urls/navigation';
|
||||
import {
|
||||
assertExceptionListsExists,
|
||||
duplicateSharedExceptionListFromListsManagementPageByListId,
|
||||
findSharedExceptionListItemsByName,
|
||||
deleteExceptionListWithoutRuleReferenceByListId,
|
||||
deleteExceptionListWithRuleReferenceByListId,
|
||||
deleteExceptionListWithoutRuleReference,
|
||||
exportExceptionList,
|
||||
waitForExceptionsTableToBeLoaded,
|
||||
createSharedExceptionList,
|
||||
|
@ -24,135 +26,211 @@ import {
|
|||
} from '../../../tasks/exceptions_table';
|
||||
import {
|
||||
EXCEPTIONS_LIST_MANAGEMENT_NAME,
|
||||
EXCEPTIONS_OVERFLOW_ACTIONS_BTN,
|
||||
EXCEPTIONS_TABLE_SHOWING_LISTS,
|
||||
} from '../../../screens/exceptions';
|
||||
import { createExceptionList } from '../../../tasks/api_calls/exceptions';
|
||||
import { createExceptionList, createExceptionListItem } from '../../../tasks/api_calls/exceptions';
|
||||
import { esArchiverResetKibana } from '../../../tasks/es_archiver';
|
||||
import { assertNumberOfExceptionItemsExists } from '../../../tasks/exceptions';
|
||||
|
||||
import { TOASTER } from '../../../screens/alerts_detection_rules';
|
||||
|
||||
const EXCEPTION_LIST_NAME = 'My shared list';
|
||||
const EXCEPTION_LIST_TO_DUPLICATE_NAME = 'A test list 2';
|
||||
const EXCEPTION_LIST_ITEM_NAME = 'Sample Exception List Item 1';
|
||||
const EXCEPTION_LIST_ITEM_NAME_2 = 'Sample Exception List Item 2';
|
||||
|
||||
const getExceptionList1 = () => ({
|
||||
...getExceptionList(),
|
||||
name: EXCEPTION_LIST_NAME,
|
||||
list_id: 'exception_list_1',
|
||||
});
|
||||
|
||||
const getExceptionList2 = () => ({
|
||||
...getExceptionList(),
|
||||
name: 'Test list 2',
|
||||
name: EXCEPTION_LIST_TO_DUPLICATE_NAME,
|
||||
list_id: 'exception_list_2',
|
||||
});
|
||||
|
||||
const expiredDate = new Date(Date.now() - 1000000).toISOString();
|
||||
const futureDate = new Date(Date.now() + 1000000).toISOString();
|
||||
|
||||
describe('Manage shared exception list', () => {
|
||||
before(() => {
|
||||
esArchiverResetKibana();
|
||||
login();
|
||||
describe('Create/Export/Delete', () => {
|
||||
before(() => {
|
||||
esArchiverResetKibana();
|
||||
login();
|
||||
|
||||
createRule(getNewRule({ name: 'Another rule' }));
|
||||
createRule(getNewRule({ name: 'Another rule' }));
|
||||
|
||||
// Create exception list associated with a rule
|
||||
createExceptionList(getExceptionList2(), getExceptionList2().list_id).then((response) =>
|
||||
createRule(
|
||||
getNewRule({
|
||||
exceptions_list: [
|
||||
{
|
||||
id: response.body.id,
|
||||
list_id: getExceptionList2().list_id,
|
||||
type: getExceptionList2().type,
|
||||
namespace_type: getExceptionList2().namespace_type,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Create exception list not used by any rules
|
||||
createExceptionList(getExceptionList1(), getExceptionList1().list_id).as(
|
||||
'exceptionListResponse'
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
visitWithoutDateRange(EXCEPTIONS_URL);
|
||||
waitForExceptionsTableToBeLoaded();
|
||||
});
|
||||
|
||||
it('Export exception list', function () {
|
||||
cy.intercept(/(\/api\/exception_lists\/_export)/).as('export');
|
||||
|
||||
exportExceptionList(getExceptionList1().list_id);
|
||||
|
||||
cy.wait('@export').then(({ response }) => {
|
||||
cy.wrap(response?.body).should(
|
||||
'eql',
|
||||
expectedExportedExceptionList(this.exceptionListResponse)
|
||||
// Create exception list associated with a rule
|
||||
createExceptionList(getExceptionList2(), getExceptionList2().list_id).then((response) =>
|
||||
createRule(
|
||||
getNewRule({
|
||||
exceptions_list: [
|
||||
{
|
||||
id: response.body.id,
|
||||
list_id: getExceptionList2().list_id,
|
||||
type: getExceptionList2().type,
|
||||
namespace_type: getExceptionList2().namespace_type,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
cy.get(TOASTER).should(
|
||||
'have.text',
|
||||
`Exception list "${EXCEPTION_LIST_NAME}" exported successfully`
|
||||
// Create exception list not used by any rules
|
||||
createExceptionList(getExceptionList1(), getExceptionList1().list_id).as(
|
||||
'exceptionListResponse'
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
visitWithoutDateRange(EXCEPTIONS_URL);
|
||||
waitForExceptionsTableToBeLoaded();
|
||||
});
|
||||
|
||||
it('Export exception list', function () {
|
||||
cy.intercept(/(\/api\/exception_lists\/_export)/).as('export');
|
||||
|
||||
exportExceptionList(getExceptionList1().list_id);
|
||||
|
||||
cy.wait('@export').then(({ response }) => {
|
||||
cy.wrap(response?.body).should(
|
||||
'eql',
|
||||
expectedExportedExceptionList(this.exceptionListResponse)
|
||||
);
|
||||
|
||||
cy.get(TOASTER).should(
|
||||
'have.text',
|
||||
`Exception list "${EXCEPTION_LIST_NAME}" exported successfully`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Link rules to shared exception list', function () {
|
||||
assertNumberLinkedRules(getExceptionList2().list_id, '1');
|
||||
linkRulesToExceptionList(getExceptionList2().list_id, 1);
|
||||
assertNumberLinkedRules(getExceptionList2().list_id, '2');
|
||||
});
|
||||
|
||||
it('Create exception list', function () {
|
||||
createSharedExceptionList(
|
||||
{ name: 'Newly created list', description: 'This is my list.' },
|
||||
true
|
||||
);
|
||||
|
||||
// After creation - directed to list detail page
|
||||
cy.get(EXCEPTIONS_LIST_MANAGEMENT_NAME).should('have.text', 'Newly created list');
|
||||
});
|
||||
|
||||
it('Delete exception list without rule reference', () => {
|
||||
// Using cy.contains because we do not care about the exact text,
|
||||
// just checking number of lists shown
|
||||
cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '4');
|
||||
|
||||
deleteExceptionListWithoutRuleReferenceByListId(getExceptionList1().list_id);
|
||||
|
||||
// Using cy.contains because we do not care about the exact text,
|
||||
// just checking number of lists shown
|
||||
cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3');
|
||||
});
|
||||
|
||||
it('Deletes exception list with rule reference', () => {
|
||||
waitForPageWithoutDateRange(EXCEPTIONS_URL);
|
||||
waitForExceptionsTableToBeLoaded();
|
||||
|
||||
// Using cy.contains because we do not care about the exact text,
|
||||
// just checking number of lists shown
|
||||
cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3');
|
||||
|
||||
deleteExceptionListWithRuleReferenceByListId(getExceptionList2().list_id);
|
||||
|
||||
// Using cy.contains because we do not care about the exact text,
|
||||
// just checking number of lists shown
|
||||
cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '2');
|
||||
});
|
||||
});
|
||||
|
||||
it('Link rules to shared exception list', function () {
|
||||
assertNumberLinkedRules(getExceptionList2().list_id, '1');
|
||||
linkRulesToExceptionList(getExceptionList2().list_id, 1);
|
||||
assertNumberLinkedRules(getExceptionList2().list_id, '2');
|
||||
});
|
||||
describe('Duplicate', () => {
|
||||
beforeEach(() => {
|
||||
esArchiverResetKibana();
|
||||
login();
|
||||
|
||||
it('Create exception list', function () {
|
||||
createSharedExceptionList({ name: EXCEPTION_LIST_NAME, description: 'This is my list.' }, true);
|
||||
// Create exception list associated with a rule
|
||||
createExceptionList(getExceptionList2(), getExceptionList2().list_id);
|
||||
|
||||
// After creation - directed to list detail page
|
||||
cy.get(EXCEPTIONS_LIST_MANAGEMENT_NAME).should('have.text', EXCEPTION_LIST_NAME);
|
||||
});
|
||||
createExceptionListItem(getExceptionList2().list_id, {
|
||||
list_id: getExceptionList2().list_id,
|
||||
item_id: 'simple_list_item_1',
|
||||
tags: [],
|
||||
type: 'simple',
|
||||
description: 'Test exception item',
|
||||
name: EXCEPTION_LIST_ITEM_NAME,
|
||||
namespace_type: 'single',
|
||||
entries: [
|
||||
{
|
||||
field: 'host.name',
|
||||
operator: 'included',
|
||||
type: 'match_any',
|
||||
value: ['some host', 'another host'],
|
||||
},
|
||||
],
|
||||
expire_time: expiredDate,
|
||||
});
|
||||
createExceptionListItem(getExceptionList2().list_id, {
|
||||
list_id: getExceptionList2().list_id,
|
||||
item_id: 'simple_list_item_2',
|
||||
tags: [],
|
||||
type: 'simple',
|
||||
description: 'Test exception item',
|
||||
name: EXCEPTION_LIST_ITEM_NAME_2,
|
||||
namespace_type: 'single',
|
||||
entries: [
|
||||
{
|
||||
field: 'host.name',
|
||||
operator: 'included',
|
||||
type: 'match_any',
|
||||
value: ['some host', 'another host'],
|
||||
},
|
||||
],
|
||||
expire_time: futureDate,
|
||||
});
|
||||
|
||||
it('Delete exception list without rule reference', () => {
|
||||
// Using cy.contains because we do not care about the exact text,
|
||||
// just checking number of lists shown
|
||||
cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '4');
|
||||
visitWithoutDateRange(EXCEPTIONS_URL);
|
||||
waitForExceptionsTableToBeLoaded();
|
||||
});
|
||||
|
||||
deleteExceptionListWithoutRuleReference();
|
||||
it('Duplicate exception list with expired items', function () {
|
||||
duplicateSharedExceptionListFromListsManagementPageByListId(
|
||||
getExceptionList2().list_id,
|
||||
true
|
||||
);
|
||||
|
||||
// Using cy.contains because we do not care about the exact text,
|
||||
// just checking number of lists shown
|
||||
cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3');
|
||||
});
|
||||
// After duplication - check for new list
|
||||
assertExceptionListsExists([`${EXCEPTION_LIST_TO_DUPLICATE_NAME} [Duplicate]`]);
|
||||
|
||||
it('Deletes exception list with rule reference', () => {
|
||||
waitForPageWithoutDateRange(EXCEPTIONS_URL);
|
||||
waitForExceptionsTableToBeLoaded();
|
||||
findSharedExceptionListItemsByName(`${EXCEPTION_LIST_TO_DUPLICATE_NAME} [Duplicate]`, [
|
||||
EXCEPTION_LIST_ITEM_NAME,
|
||||
EXCEPTION_LIST_ITEM_NAME_2,
|
||||
]);
|
||||
|
||||
// Using cy.contains because we do not care about the exact text,
|
||||
// just checking number of lists shown
|
||||
cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3');
|
||||
assertNumberOfExceptionItemsExists(2);
|
||||
});
|
||||
|
||||
deleteExceptionListWithRuleReferenceByListId(getExceptionList2().list_id);
|
||||
it('Duplicate exception list without expired items', function () {
|
||||
duplicateSharedExceptionListFromListsManagementPageByListId(
|
||||
getExceptionList2().list_id,
|
||||
false
|
||||
);
|
||||
|
||||
// Using cy.contains because we do not care about the exact text,
|
||||
// just checking number of lists shown
|
||||
cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Manage shared exception list - read only', () => {
|
||||
before(() => {
|
||||
// First we login as a privileged user to create exception list
|
||||
esArchiverResetKibana();
|
||||
login(ROLES.platform_engineer);
|
||||
visitWithoutDateRange(EXCEPTIONS_URL, ROLES.platform_engineer);
|
||||
createExceptionList(getExceptionList(), getExceptionList().list_id);
|
||||
|
||||
// Then we login as read-only user to test.
|
||||
login(ROLES.reader);
|
||||
visitWithoutDateRange(EXCEPTIONS_URL, ROLES.reader);
|
||||
waitForExceptionsTableToBeLoaded();
|
||||
|
||||
cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`);
|
||||
});
|
||||
|
||||
it('Exception list actions should be disabled', () => {
|
||||
cy.get(EXCEPTIONS_OVERFLOW_ACTIONS_BTN).first().should('be.disabled');
|
||||
// After duplication - check for new list
|
||||
assertExceptionListsExists([`${EXCEPTION_LIST_TO_DUPLICATE_NAME} [Duplicate]`]);
|
||||
|
||||
findSharedExceptionListItemsByName(`${EXCEPTION_LIST_TO_DUPLICATE_NAME} [Duplicate]`, [
|
||||
EXCEPTION_LIST_ITEM_NAME_2,
|
||||
]);
|
||||
|
||||
assertNumberOfExceptionItemsExists(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,6 +31,15 @@ export interface ExceptionListItem {
|
|||
tags: string[];
|
||||
type: 'simple';
|
||||
entries: Array<{ field: string; operator: string; type: string; value: string[] }>;
|
||||
expire_time?: string;
|
||||
}
|
||||
|
||||
export interface RuleExceptionItem {
|
||||
description: string;
|
||||
name: string;
|
||||
type: 'simple';
|
||||
entries: Array<{ field: string; operator: string; type: string; value: string[] | string }>;
|
||||
expire_time: string | undefined;
|
||||
}
|
||||
|
||||
export const getExceptionList = (): ExceptionList => ({
|
||||
|
|
|
@ -38,6 +38,13 @@ export const DELETE_RULE_BULK_BTN = '[data-test-subj="deleteRuleBulk"]';
|
|||
|
||||
export const DUPLICATE_RULE_BULK_BTN = '[data-test-subj="duplicateRuleBulk"]';
|
||||
|
||||
export const DUPLICATE_WITH_EXCEPTIONS_OPTION = '[data-test-subj="withExceptions"] label';
|
||||
|
||||
export const DUPLICATE_WITH_EXCEPTIONS_WITHOUT_EXPIRED_OPTION =
|
||||
'[data-test-subj="withExceptionsExcludeExpiredExceptions"] label';
|
||||
|
||||
export const DUPLICATE_WITHOUT_EXCEPTIONS_OPTION = '[data-test-subj="withoutExceptions"] label';
|
||||
|
||||
export const RULE_SEARCH_FIELD = '[data-test-subj="ruleSearchField"]';
|
||||
|
||||
export const EXPORT_ACTION_BTN = '[data-test-subj="exportRuleAction"]';
|
||||
|
|
|
@ -55,7 +55,14 @@ export const EXCEPTIONS_TABLE_LINK_RULES_BTN =
|
|||
export const EXCEPTIONS_TABLE_EXPORT_MODAL_BTN =
|
||||
'[data-test-subj="sharedListOverflowCardActionItemExport"]';
|
||||
|
||||
export const EXCEPTIONS_TABLE_EXPORT_CONFIRM_BTN = '[data-test-subj="confirmModalConfirmButton"]';
|
||||
export const EXCEPTIONS_TABLE_DUPLICATE_BTN =
|
||||
'[data-test-subj="sharedListOverflowCardActionItemDuplicate"]';
|
||||
|
||||
export const EXCEPTIONS_TABLE_EXPIRED_EXCEPTION_ITEMS_MODAL_CONFIRM_BTN =
|
||||
'[data-test-subj="confirmModalConfirmButton"]';
|
||||
|
||||
export const INCLUDE_EXPIRED_EXCEPTION_ITEMS_SWITCH =
|
||||
'[data-test-subj="includeExpiredExceptionsConfirmationModalSwitch"]';
|
||||
|
||||
export const EXCEPTIONS_TABLE_SEARCH_CLEAR =
|
||||
'[data-test-subj="allExceptionListsPanel"] button.euiFormControlLayoutClearButton';
|
||||
|
@ -179,3 +186,5 @@ export const EXCEPTIONS_LIST_EDIT_DETAILS_SAVE_BTN = '[data-test-subj="editModal
|
|||
|
||||
export const EXCEPTIONS_LIST_DETAILS_HEADER =
|
||||
'[data-test-subj="exceptionListManagementPageHeader"]';
|
||||
|
||||
export const EXCEPTION_LIST_DETAILS_CARD_ITEM_NAME = '[data-test-subj="exceptionItemCardHeader"]';
|
||||
|
|
|
@ -39,6 +39,12 @@ export const DETAILS_TITLE = '.euiDescriptionList__title';
|
|||
|
||||
export const EXCEPTIONS_TAB = 'a[data-test-subj="navigation-rule_exceptions"]';
|
||||
|
||||
export const EXCEPTIONS_TAB_EXPIRED_FILTER = '[data-test-subj="expired"]';
|
||||
|
||||
export const EXCEPTIONS_TAB_ACTIVE_FILTER = '[data-test-subj="active"]';
|
||||
|
||||
export const EXCEPTIONS_ITEM_CONTAINER = '[data-test-subj="exceptionsContainer"]';
|
||||
|
||||
export const FALSE_POSITIVES_DETAILS = 'False positive examples';
|
||||
|
||||
export const INDEX_PATTERNS_DETAILS = 'Index patterns';
|
||||
|
|
|
@ -62,6 +62,9 @@ import {
|
|||
DISABLED_RULES_BTN,
|
||||
REFRESH_RULES_TABLE_BUTTON,
|
||||
RULE_LAST_RUN,
|
||||
DUPLICATE_WITHOUT_EXCEPTIONS_OPTION,
|
||||
DUPLICATE_WITH_EXCEPTIONS_OPTION,
|
||||
DUPLICATE_WITH_EXCEPTIONS_WITHOUT_EXPIRED_OPTION,
|
||||
} from '../screens/alerts_detection_rules';
|
||||
import type { RULES_MONITORING_TABLE } from '../screens/alerts_detection_rules';
|
||||
import { EUI_CHECKBOX } from '../screens/common/controls';
|
||||
|
@ -145,10 +148,27 @@ export const deleteRuleFromDetailsPage = () => {
|
|||
.should(($el) => expect($el).to.be.not.visible);
|
||||
};
|
||||
|
||||
export const duplicateSelectedRules = () => {
|
||||
export const duplicateSelectedRulesWithoutExceptions = () => {
|
||||
cy.log('Duplicate selected rules');
|
||||
cy.get(BULK_ACTIONS_BTN).click({ force: true });
|
||||
cy.get(DUPLICATE_RULE_BULK_BTN).click();
|
||||
cy.get(DUPLICATE_WITHOUT_EXCEPTIONS_OPTION).click();
|
||||
cy.get(CONFIRM_DUPLICATE_RULE).click();
|
||||
};
|
||||
|
||||
export const duplicateSelectedRulesWithExceptions = () => {
|
||||
cy.log('Duplicate selected rules');
|
||||
cy.get(BULK_ACTIONS_BTN).click({ force: true });
|
||||
cy.get(DUPLICATE_RULE_BULK_BTN).click();
|
||||
cy.get(DUPLICATE_WITH_EXCEPTIONS_OPTION).click();
|
||||
cy.get(CONFIRM_DUPLICATE_RULE).click();
|
||||
};
|
||||
|
||||
export const duplicateSelectedRulesWithNonExpiredExceptions = () => {
|
||||
cy.log('Duplicate selected rules');
|
||||
cy.get(BULK_ACTIONS_BTN).click({ force: true });
|
||||
cy.get(DUPLICATE_RULE_BULK_BTN).click();
|
||||
cy.get(DUPLICATE_WITH_EXCEPTIONS_WITHOUT_EXPIRED_OPTION).click();
|
||||
cy.get(CONFIRM_DUPLICATE_RULE).click();
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ExceptionList, ExceptionListItem } from '../../objects/exception';
|
||||
import type { ExceptionList, ExceptionListItem, RuleExceptionItem } from '../../objects/exception';
|
||||
|
||||
export const createEndpointExceptionList = () =>
|
||||
cy.request({
|
||||
|
@ -59,6 +59,18 @@ export const createExceptionListItem = (
|
|||
value: ['some host', 'another host'],
|
||||
},
|
||||
],
|
||||
expire_time: exceptionListItem?.expire_time,
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds' },
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
|
||||
export const createRuleExceptionItem = (ruleId: string, exceptionListItems: RuleExceptionItem[]) =>
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `/api/detection_engine/rules/${ruleId}/exceptions`,
|
||||
body: {
|
||||
items: exceptionListItems,
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds' },
|
||||
failOnStatusCode: false,
|
||||
|
|
|
@ -28,8 +28,24 @@ import {
|
|||
EXCEPTION_FIELD_MAPPING_CONFLICTS_TOOLTIP,
|
||||
EXCEPTION_FIELD_MAPPING_CONFLICTS_ACCORDION_ICON,
|
||||
EXCEPTION_FIELD_MAPPING_CONFLICTS_DESCRIPTION,
|
||||
EXCEPTION_ITEM_VIEWER_CONTAINER,
|
||||
} from '../screens/exceptions';
|
||||
|
||||
export const assertNumberOfExceptionItemsExists = (numberOfItems: number) => {
|
||||
cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', numberOfItems);
|
||||
};
|
||||
|
||||
export const expectToContainItem = (container: string, itemName: string) => {
|
||||
cy.log(`Expecting exception items table to contain '${itemName}'`);
|
||||
cy.get(container).should('include.text', itemName);
|
||||
};
|
||||
|
||||
export const assertExceptionItemsExists = (container: string, itemNames: string[]) => {
|
||||
for (const itemName of itemNames) {
|
||||
expectToContainItem(container, itemName);
|
||||
}
|
||||
};
|
||||
|
||||
export const addExceptionEntryFieldValueOfItemX = (
|
||||
field: string,
|
||||
itemIndex = 0,
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN,
|
||||
EXCEPTIONS_TABLE_EXPORT_MODAL_BTN,
|
||||
EXCEPTIONS_OVERFLOW_ACTIONS_BTN,
|
||||
EXCEPTIONS_TABLE_EXPORT_CONFIRM_BTN,
|
||||
EXCEPTIONS_TABLE_EXPIRED_EXCEPTION_ITEMS_MODAL_CONFIRM_BTN,
|
||||
MANAGE_EXCEPTION_CREATE_BUTTON_MENU,
|
||||
MANAGE_EXCEPTION_CREATE_LIST_BUTTON,
|
||||
CREATE_SHARED_EXCEPTION_LIST_NAME_INPUT,
|
||||
|
@ -26,12 +26,17 @@ import {
|
|||
EXCEPTIONS_LIST_MANAGEMENT_EDIT_MODAL_DESCRIPTION_INPUT,
|
||||
EXCEPTIONS_LIST_EDIT_DETAILS_SAVE_BTN,
|
||||
EXCEPTIONS_LIST_DETAILS_HEADER,
|
||||
EXCEPTIONS_TABLE_DUPLICATE_BTN,
|
||||
EXCEPTIONS_TABLE_LIST_NAME,
|
||||
INCLUDE_EXPIRED_EXCEPTION_ITEMS_SWITCH,
|
||||
EXCEPTION_LIST_DETAILS_CARD_ITEM_NAME,
|
||||
exceptionsTableListManagementListContainerByListId,
|
||||
EXCEPTIONS_TABLE_LINK_RULES_BTN,
|
||||
RULE_ACTION_LINK_RULE_SWITCH,
|
||||
LINKED_RULES_BADGE,
|
||||
MANAGE_RULES_SAVE,
|
||||
} from '../screens/exceptions';
|
||||
import { assertExceptionItemsExists } from './exceptions';
|
||||
|
||||
export const clearSearchSelection = () => {
|
||||
cy.get(EXCEPTIONS_TABLE_SEARCH_CLEAR).first().click();
|
||||
|
@ -46,7 +51,7 @@ export const exportExceptionList = (listId: string) => {
|
|||
.find(EXCEPTIONS_OVERFLOW_ACTIONS_BTN)
|
||||
.click();
|
||||
cy.get(EXCEPTIONS_TABLE_EXPORT_MODAL_BTN).first().click();
|
||||
cy.get(EXCEPTIONS_TABLE_EXPORT_CONFIRM_BTN).first().click();
|
||||
cy.get(EXCEPTIONS_TABLE_EXPIRED_EXCEPTION_ITEMS_MODAL_CONFIRM_BTN).first().click();
|
||||
};
|
||||
|
||||
export const assertNumberLinkedRules = (listId: string, numberOfRulesAsString: string) => {
|
||||
|
@ -65,8 +70,10 @@ export const linkRulesToExceptionList = (listId: string, ruleSwitch: number = 0)
|
|||
cy.get(MANAGE_RULES_SAVE).first().click();
|
||||
};
|
||||
|
||||
export const deleteExceptionListWithoutRuleReference = () => {
|
||||
cy.get(EXCEPTIONS_OVERFLOW_ACTIONS_BTN).first().click();
|
||||
export const deleteExceptionListWithoutRuleReferenceByListId = (listId: string) => {
|
||||
cy.get(exceptionsTableListManagementListContainerByListId(listId))
|
||||
.find(EXCEPTIONS_OVERFLOW_ACTIONS_BTN)
|
||||
.click();
|
||||
cy.get(EXCEPTIONS_TABLE_DELETE_BTN).first().click();
|
||||
cy.get(EXCEPTIONS_TABLE_MODAL).should('exist');
|
||||
cy.get(EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN).first().click();
|
||||
|
@ -117,10 +124,42 @@ export const createSharedExceptionList = (
|
|||
}
|
||||
};
|
||||
|
||||
export const expectToContainList = (listName: string) => {
|
||||
cy.log(`Expecting exception lists table to contain '${listName}'`);
|
||||
cy.get(EXCEPTIONS_TABLE_LIST_NAME).should('include.text', listName);
|
||||
};
|
||||
|
||||
export const assertExceptionListsExists = (listNames: string[]) => {
|
||||
for (const listName of listNames) {
|
||||
expectToContainList(listName);
|
||||
}
|
||||
};
|
||||
|
||||
export const duplicateSharedExceptionListFromListsManagementPageByListId = (
|
||||
listId: string,
|
||||
includeExpired: boolean
|
||||
) => {
|
||||
cy.log(`Duplicating list with list_id: '${listId}'`);
|
||||
cy.get(exceptionsTableListManagementListContainerByListId(listId))
|
||||
.find(EXCEPTIONS_OVERFLOW_ACTIONS_BTN)
|
||||
.click();
|
||||
cy.get(EXCEPTIONS_TABLE_DUPLICATE_BTN).first().click();
|
||||
if (!includeExpired) {
|
||||
cy.get(INCLUDE_EXPIRED_EXCEPTION_ITEMS_SWITCH).first().click();
|
||||
}
|
||||
cy.get(EXCEPTIONS_TABLE_EXPIRED_EXCEPTION_ITEMS_MODAL_CONFIRM_BTN).first().click();
|
||||
};
|
||||
|
||||
export const waitForExceptionListDetailToBeLoaded = () => {
|
||||
cy.get(EXCEPTIONS_LIST_DETAILS_HEADER).should('exist');
|
||||
};
|
||||
|
||||
export const findSharedExceptionListItemsByName = (listName: string, itemNames: string[]) => {
|
||||
cy.contains(listName).click();
|
||||
waitForExceptionListDetailToBeLoaded();
|
||||
assertExceptionItemsExists(EXCEPTION_LIST_DETAILS_CARD_ITEM_NAME, itemNames);
|
||||
};
|
||||
|
||||
export const editExceptionLisDetails = ({
|
||||
name,
|
||||
description,
|
||||
|
|
|
@ -29,6 +29,8 @@ import {
|
|||
ENDPOINT_EXCEPTIONS_TAB,
|
||||
EDIT_RULE_SETTINGS_LINK,
|
||||
BACK_TO_RULES_TABLE,
|
||||
EXCEPTIONS_TAB_EXPIRED_FILTER,
|
||||
EXCEPTIONS_TAB_ACTIVE_FILTER,
|
||||
} from '../screens/rule_details';
|
||||
import {
|
||||
addExceptionConditions,
|
||||
|
@ -105,6 +107,11 @@ export const goToExceptionsTab = () => {
|
|||
cy.get(EXCEPTIONS_TAB).click();
|
||||
};
|
||||
|
||||
export const viewExpiredExceptionItems = () => {
|
||||
cy.get(EXCEPTIONS_TAB_EXPIRED_FILTER).click();
|
||||
cy.get(EXCEPTIONS_TAB_ACTIVE_FILTER).click();
|
||||
};
|
||||
|
||||
export const goToEndpointExceptionsTab = () => {
|
||||
cy.get(ENDPOINT_EXCEPTIONS_TAB).should('exist');
|
||||
cy.get(ENDPOINT_EXCEPTIONS_TAB).click();
|
||||
|
|
|
@ -46,21 +46,35 @@ const BulkActionDuplicateExceptionsConfirmationComponent = ({
|
|||
defaultFocusedButton="confirm"
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<EuiText>
|
||||
{i18n.MODAL_TEXT(rulesCount)}{' '}
|
||||
<EuiIconTip content={i18n.DUPLICATE_TOOLTIP} position="bottom" />
|
||||
</EuiText>
|
||||
<EuiText>{i18n.MODAL_TEXT(rulesCount)}</EuiText>
|
||||
|
||||
<EuiSpacer />
|
||||
<EuiRadioGroup
|
||||
options={[
|
||||
{
|
||||
id: DuplicateOptions.withExceptions,
|
||||
label: i18n.DUPLICATE_EXCEPTIONS_TEXT(rulesCount),
|
||||
label: (
|
||||
<EuiText size="s">
|
||||
{i18n.DUPLICATE_EXCEPTIONS_INCLUDE_EXPIRED_EXCEPTIONS_LABEL(rulesCount)}
|
||||
<EuiIconTip content={i18n.DUPLICATE_TOOLTIP} position="bottom" />
|
||||
</EuiText>
|
||||
),
|
||||
'data-test-subj': DuplicateOptions.withExceptions,
|
||||
},
|
||||
{
|
||||
id: DuplicateOptions.withExceptionsExcludeExpiredExceptions,
|
||||
label: (
|
||||
<EuiText size="s">
|
||||
{i18n.DUPLICATE_EXCEPTIONS_TEXT(rulesCount)}
|
||||
<EuiIconTip content={i18n.DUPLICATE_TOOLTIP} position="bottom" />
|
||||
</EuiText>
|
||||
),
|
||||
'data-test-subj': DuplicateOptions.withExceptionsExcludeExpiredExceptions,
|
||||
},
|
||||
{
|
||||
id: DuplicateOptions.withoutExceptions,
|
||||
label: i18n.DUPLICATE_WITHOUT_EXCEPTIONS_TEXT(rulesCount),
|
||||
'data-test-subj': DuplicateOptions.withoutExceptions,
|
||||
},
|
||||
]}
|
||||
idSelected={selectedDuplicateOption}
|
||||
|
|
|
@ -138,8 +138,8 @@ 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?"
|
||||
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exception.confirmation.modalTitle"
|
||||
defaultMessage="Duplicate {rulesCount, plural, one {the rule} other {rules}}?"
|
||||
values={{ rulesCount }}
|
||||
/>
|
||||
),
|
||||
|
@ -147,7 +147,7 @@ export const bulkDuplicateRuleActions = {
|
|||
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"
|
||||
defaultMessage="You're duplicating {rulesCount, plural, one {# rule} other {# rules}}. Choose what to duplicate:"
|
||||
values={{ rulesCount }}
|
||||
/>
|
||||
),
|
||||
|
@ -155,7 +155,15 @@ export const bulkDuplicateRuleActions = {
|
|||
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"
|
||||
defaultMessage="The {rulesCount, plural, one {rule} other {rules}} and {rulesCount, plural, one {its} other {their}} active exceptions"
|
||||
values={{ rulesCount }}
|
||||
/>
|
||||
),
|
||||
|
||||
DUPLICATE_EXCEPTIONS_INCLUDE_EXPIRED_EXCEPTIONS_LABEL: (rulesCount: number) => (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.includeExpiredExceptionsCheckboxLabel"
|
||||
defaultMessage="The {rulesCount, plural, one {rule} other {rules}} and all of {rulesCount, plural, one {its} other {their}} exceptions (active and expired)"
|
||||
values={{ rulesCount }}
|
||||
/>
|
||||
),
|
||||
|
@ -163,7 +171,7 @@ export const bulkDuplicateRuleActions = {
|
|||
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}}"
|
||||
defaultMessage="Only the {rulesCount, plural, one {rule} other {rules}}"
|
||||
values={{ rulesCount }}
|
||||
/>
|
||||
),
|
||||
|
@ -186,7 +194,7 @@ export const bulkDuplicateRuleActions = {
|
|||
'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',
|
||||
' If you choose to duplicate exceptions, the shared exceptions list will be duplicated by reference and the rule exceptions will be copied and created anew',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
|
|
@ -137,7 +137,13 @@ export const useBulkActions = ({
|
|||
type: BulkActionType.duplicate,
|
||||
duplicatePayload: {
|
||||
include_exceptions:
|
||||
modalDuplicationConfirmationResult === DuplicateOptions.withExceptions,
|
||||
modalDuplicationConfirmationResult === DuplicateOptions.withExceptions ||
|
||||
modalDuplicationConfirmationResult ===
|
||||
DuplicateOptions.withExceptionsExcludeExpiredExceptions,
|
||||
include_expired_exceptions: !(
|
||||
modalDuplicationConfirmationResult ===
|
||||
DuplicateOptions.withExceptionsExcludeExpiredExceptions
|
||||
),
|
||||
},
|
||||
...(isAllSelected ? { query: filterQuery } : { ids: selectedRuleIds }),
|
||||
});
|
||||
|
|
|
@ -77,7 +77,13 @@ export const useRulesTableActions = ({
|
|||
ids: [rule.id],
|
||||
duplicatePayload: {
|
||||
include_exceptions:
|
||||
modalDuplicationConfirmationResult === DuplicateOptions.withExceptions,
|
||||
modalDuplicationConfirmationResult === DuplicateOptions.withExceptions ||
|
||||
modalDuplicationConfirmationResult ===
|
||||
DuplicateOptions.withExceptionsExcludeExpiredExceptions,
|
||||
include_expired_exceptions: !(
|
||||
modalDuplicationConfirmationResult ===
|
||||
DuplicateOptions.withExceptionsExcludeExpiredExceptions
|
||||
),
|
||||
},
|
||||
});
|
||||
const createdRules = result?.attributes.results.created;
|
||||
|
|
|
@ -96,7 +96,13 @@ const RuleActionsOverflowComponent = ({
|
|||
ids: [rule.id],
|
||||
duplicatePayload: {
|
||||
include_exceptions:
|
||||
modalDuplicationConfirmationResult === DuplicateOptions.withExceptions,
|
||||
modalDuplicationConfirmationResult === DuplicateOptions.withExceptions ||
|
||||
modalDuplicationConfirmationResult ===
|
||||
DuplicateOptions.withExceptionsExcludeExpiredExceptions,
|
||||
include_expired_exceptions: !(
|
||||
modalDuplicationConfirmationResult ===
|
||||
DuplicateOptions.withExceptionsExcludeExpiredExceptions
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { ExceptionsListCard } from '.';
|
||||
import { useListDetailsView } from '../../hooks';
|
||||
import { useExceptionsListCard } from '../../hooks/use_exceptions_list.card';
|
||||
import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
|
||||
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
jest.mock('../../hooks');
|
||||
jest.mock('../../hooks/use_exceptions_list.card');
|
||||
|
||||
const getMockUseExceptionsListCard = () => ({
|
||||
listId: 'my-list',
|
||||
listName: 'Exception list',
|
||||
listType: 'detection',
|
||||
createdAt: '2023-02-01T10:20:30.000Z',
|
||||
createdBy: 'elastic',
|
||||
exceptions: [{ ...getExceptionListItemSchemaMock() }],
|
||||
pagination: { pageIndex: 0, pageSize: 5, totalItemCount: 1 },
|
||||
ruleReferences: {
|
||||
'my-list': {
|
||||
name: 'Exception list',
|
||||
id: '345',
|
||||
referenced_rules: [],
|
||||
listId: 'my-list',
|
||||
},
|
||||
},
|
||||
toggleAccordion: false,
|
||||
openAccordionId: '123',
|
||||
menuActionItems: [
|
||||
{
|
||||
key: 'Export',
|
||||
icon: 'exportAction',
|
||||
label: 'Export',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
],
|
||||
listRulesCount: '5',
|
||||
listDescription: 'My exception list description',
|
||||
exceptionItemsCount: jest.fn(),
|
||||
onEditExceptionItem: jest.fn(),
|
||||
onDeleteException: jest.fn(),
|
||||
onPaginationChange: jest.fn(),
|
||||
setToggleAccordion: jest.fn(),
|
||||
exceptionViewerStatus: '',
|
||||
showAddExceptionFlyout: false,
|
||||
showEditExceptionFlyout: false,
|
||||
exceptionToEdit: undefined,
|
||||
onAddExceptionClick: jest.fn(),
|
||||
handleConfirmExceptionFlyout: jest.fn(),
|
||||
handleCancelExceptionItemFlyout: jest.fn(),
|
||||
goToExceptionDetail: jest.fn(),
|
||||
emptyViewerTitle: 'Empty View',
|
||||
emptyViewerBody: 'This is the empty view description.',
|
||||
emptyViewerButtonText: 'Take action',
|
||||
handleCancelExpiredExceptionsModal: jest.fn(),
|
||||
handleConfirmExpiredExceptionsModal: jest.fn(),
|
||||
showIncludeExpiredExceptionsModal: false,
|
||||
});
|
||||
const getMockUseListDetailsView = () => ({
|
||||
linkedRules: [],
|
||||
showManageRulesFlyout: false,
|
||||
showManageButtonLoader: false,
|
||||
disableManageButton: false,
|
||||
onManageRules: jest.fn(),
|
||||
onSaveManageRules: jest.fn(),
|
||||
onCancelManageRules: jest.fn(),
|
||||
onRuleSelectionChange: jest.fn(),
|
||||
});
|
||||
|
||||
describe('ExceptionsListCard', () => {
|
||||
beforeEach(() => {
|
||||
(useExceptionsListCard as jest.Mock).mockReturnValue(getMockUseExceptionsListCard());
|
||||
(useListDetailsView as jest.Mock).mockReturnValue(getMockUseListDetailsView());
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should display expired exception confirmation modal when "showIncludeExpiredExceptionsModal" is "true"', () => {
|
||||
(useExceptionsListCard as jest.Mock).mockReturnValue({
|
||||
...getMockUseExceptionsListCard(),
|
||||
showIncludeExpiredExceptionsModal: true,
|
||||
});
|
||||
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<ExceptionsListCard
|
||||
exceptionsList={{ ...getExceptionListSchemaMock(), rules: [] }}
|
||||
handleDelete={jest.fn()}
|
||||
handleExport={jest.fn()}
|
||||
handleDuplicate={jest.fn()}
|
||||
readOnly={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.getByTestId('includeExpiredExceptionsConfirmationModal')).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -33,7 +33,7 @@ import { ListExceptionItems } from '../list_exception_items';
|
|||
import { useListDetailsView } from '../../hooks';
|
||||
import { useExceptionsListCard } from '../../hooks/use_exceptions_list.card';
|
||||
import { ManageRules } from '../manage_rules';
|
||||
import { ExportExceptionsListModal } from '../export_exceptions_list_modal';
|
||||
import { IncludeExpiredExceptionsModal } from '../expired_exceptions_list_items_modal';
|
||||
|
||||
interface ExceptionsListCardProps {
|
||||
exceptionsList: ExceptionListInfo;
|
||||
|
@ -59,6 +59,17 @@ interface ExceptionsListCardProps {
|
|||
name: string;
|
||||
namespaceType: NamespaceType;
|
||||
}) => () => Promise<void>;
|
||||
handleDuplicate: ({
|
||||
includeExpiredExceptions,
|
||||
listId,
|
||||
name,
|
||||
namespaceType,
|
||||
}: {
|
||||
includeExpiredExceptions: boolean;
|
||||
listId: string;
|
||||
name: string;
|
||||
namespaceType: NamespaceType;
|
||||
}) => () => Promise<void>;
|
||||
readOnly: boolean;
|
||||
}
|
||||
const buttonCss = css`
|
||||
|
@ -78,7 +89,7 @@ const ListHeaderContainer = styled(EuiFlexGroup)`
|
|||
text-align: initial;
|
||||
`;
|
||||
export const ExceptionsListCard = memo<ExceptionsListCardProps>(
|
||||
({ exceptionsList, handleDelete, handleExport, readOnly }) => {
|
||||
({ exceptionsList, handleDelete, handleExport, handleDuplicate, readOnly }) => {
|
||||
const {
|
||||
linkedRules,
|
||||
showManageRulesFlyout,
|
||||
|
@ -119,13 +130,14 @@ export const ExceptionsListCard = memo<ExceptionsListCardProps>(
|
|||
emptyViewerTitle,
|
||||
emptyViewerBody,
|
||||
emptyViewerButtonText,
|
||||
handleCancelExportModal,
|
||||
handleConfirmExportModal,
|
||||
showExportModal,
|
||||
handleCancelExpiredExceptionsModal,
|
||||
handleConfirmExpiredExceptionsModal,
|
||||
showIncludeExpiredExceptionsModal,
|
||||
} = useExceptionsListCard({
|
||||
exceptionsList,
|
||||
handleExport,
|
||||
handleDelete,
|
||||
handleDuplicate,
|
||||
handleManageRules: onManageRules,
|
||||
});
|
||||
|
||||
|
@ -259,10 +271,11 @@ export const ExceptionsListCard = memo<ExceptionsListCardProps>(
|
|||
onRuleSelectionChange={onRuleSelectionChange}
|
||||
/>
|
||||
) : null}
|
||||
{showExportModal ? (
|
||||
<ExportExceptionsListModal
|
||||
handleCloseModal={handleCancelExportModal}
|
||||
onModalConfirm={handleConfirmExportModal}
|
||||
{showIncludeExpiredExceptionsModal ? (
|
||||
<IncludeExpiredExceptionsModal
|
||||
handleCloseModal={handleCancelExpiredExceptionsModal}
|
||||
onModalConfirm={handleConfirmExpiredExceptionsModal}
|
||||
action={showIncludeExpiredExceptionsModal}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { IncludeExpiredExceptionsModal } from '.';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
describe('IncludeExpiredExceptionsModal', () => {
|
||||
const handleCloseModal = jest.fn();
|
||||
const onModalConfirm = jest.fn();
|
||||
|
||||
it('should call handleCloseModal on cancel click', () => {
|
||||
const wrapper = render(
|
||||
<IncludeExpiredExceptionsModal
|
||||
handleCloseModal={handleCloseModal}
|
||||
onModalConfirm={onModalConfirm}
|
||||
action={'export'}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(wrapper.getByTestId('confirmModalCancelButton'));
|
||||
expect(handleCloseModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onModalConfirm on confirm click', () => {
|
||||
const wrapper = render(
|
||||
<IncludeExpiredExceptionsModal
|
||||
handleCloseModal={handleCloseModal}
|
||||
onModalConfirm={onModalConfirm}
|
||||
action={'duplicate'}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(wrapper.getByTestId('confirmModalConfirmButton'));
|
||||
expect(onModalConfirm).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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, { memo, useCallback, useState } from 'react';
|
||||
|
||||
import { EuiConfirmModal, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
export const CHECK_EXCEPTION_TTL_ACTION_TYPES = {
|
||||
DUPLICATE: 'duplicate',
|
||||
EXPORT: 'export',
|
||||
} as const;
|
||||
|
||||
export type CheckExceptionTtlActionTypes =
|
||||
typeof CHECK_EXCEPTION_TTL_ACTION_TYPES[keyof typeof CHECK_EXCEPTION_TTL_ACTION_TYPES];
|
||||
|
||||
interface IncludeExpiredExceptionsModalProps {
|
||||
handleCloseModal: () => void;
|
||||
onModalConfirm: (includeExpired: boolean) => void;
|
||||
action: CheckExceptionTtlActionTypes;
|
||||
}
|
||||
|
||||
export const IncludeExpiredExceptionsModal = memo<IncludeExpiredExceptionsModalProps>(
|
||||
({ handleCloseModal, onModalConfirm, action }) => {
|
||||
const [includeExpired, setIncludeExpired] = useState(true);
|
||||
|
||||
const handleSwitchChange = useCallback(() => {
|
||||
setIncludeExpired(!includeExpired);
|
||||
}, [setIncludeExpired, includeExpired]);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onModalConfirm(includeExpired);
|
||||
handleCloseModal();
|
||||
}, [includeExpired, handleCloseModal, onModalConfirm]);
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
title={
|
||||
action === CHECK_EXCEPTION_TTL_ACTION_TYPES.EXPORT
|
||||
? i18n.EXPIRED_EXCEPTIONS_MODAL_EXPORT_TITLE
|
||||
: i18n.EXPIRED_EXCEPTIONS_MODAL_DUPLICATE_TITLE
|
||||
}
|
||||
onCancel={handleCloseModal}
|
||||
onConfirm={handleConfirm}
|
||||
cancelButtonText={i18n.EXPIRED_EXCEPTIONS_MODAL_CANCEL_BUTTON}
|
||||
confirmButtonText={
|
||||
action === CHECK_EXCEPTION_TTL_ACTION_TYPES.EXPORT
|
||||
? i18n.EXPIRED_EXCEPTIONS_MODAL_CONFIRM_EXPORT_BUTTON
|
||||
: i18n.EXPIRED_EXCEPTIONS_MODAL_CONFIRM_DUPLICATE_BUTTON
|
||||
}
|
||||
defaultFocusedButton="confirm"
|
||||
data-test-subj="includeExpiredExceptionsConfirmationModal"
|
||||
>
|
||||
<EuiText>
|
||||
{action === CHECK_EXCEPTION_TTL_ACTION_TYPES.EXPORT
|
||||
? i18n.EXPIRED_EXCEPTIONS_MODAL_EXPORT_DESCRIPTION
|
||||
: i18n.EXPIRED_EXCEPTIONS_MODAL_DUPLICATE_DESCRIPTION}
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
label={i18n.EXPIRED_EXCEPTIONS_MODAL_INCLUDE_SWITCH_LABEL}
|
||||
checked={includeExpired}
|
||||
onChange={handleSwitchChange}
|
||||
data-test-subj="includeExpiredExceptionsConfirmationModalSwitch"
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
IncludeExpiredExceptionsModal.displayName = 'IncludeExpiredExceptionsModal';
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback, useState } from 'react';
|
||||
|
||||
import { EuiConfirmModal, EuiSwitch } from '@elastic/eui';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
interface ExportExceptionsListModalProps {
|
||||
handleCloseModal: () => void;
|
||||
onModalConfirm: (includeExpired: boolean) => void;
|
||||
}
|
||||
|
||||
export const ExportExceptionsListModal = memo<ExportExceptionsListModalProps>(
|
||||
({ handleCloseModal, onModalConfirm }) => {
|
||||
const [exportExpired, setExportExpired] = useState(true);
|
||||
|
||||
const handleSwitchChange = useCallback(() => {
|
||||
setExportExpired(!exportExpired);
|
||||
}, [setExportExpired, exportExpired]);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onModalConfirm(exportExpired);
|
||||
handleCloseModal();
|
||||
}, [exportExpired, handleCloseModal, onModalConfirm]);
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
title={i18n.EXPORT_MODAL_TITLE}
|
||||
onCancel={handleCloseModal}
|
||||
onConfirm={handleConfirm}
|
||||
cancelButtonText={i18n.EXPORT_MODAL_CANCEL_BUTTON}
|
||||
confirmButtonText={i18n.EXPORT_MODAL_CONFIRM_BUTTON}
|
||||
defaultFocusedButton="confirm"
|
||||
>
|
||||
<EuiSwitch
|
||||
label={i18n.EXPORT_MODAL_INCLUDE_SWITCH_LABEL}
|
||||
checked={exportExpired}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ExportExceptionsListModal.displayName = 'ExportExceptionsListModal';
|
|
@ -20,7 +20,15 @@ import type { ExceptionListInfo } from '../use_all_exception_lists';
|
|||
import { useListExceptionItems } from '../use_list_exception_items';
|
||||
import * as i18n from '../../translations';
|
||||
import { checkIfListCannotBeEdited } from '../../utils/list.utils';
|
||||
import type { CheckExceptionTtlActionTypes } from '../../components/expired_exceptions_list_items_modal';
|
||||
import { CHECK_EXCEPTION_TTL_ACTION_TYPES } from '../../components/expired_exceptions_list_items_modal';
|
||||
|
||||
interface DuplicateListAction {
|
||||
listId: string;
|
||||
name: string;
|
||||
namespaceType: NamespaceType;
|
||||
includeExpiredExceptions: boolean;
|
||||
}
|
||||
interface ExportListAction {
|
||||
id: string;
|
||||
listId: string;
|
||||
|
@ -37,6 +45,7 @@ export const useExceptionsListCard = ({
|
|||
exceptionsList,
|
||||
handleExport,
|
||||
handleDelete,
|
||||
handleDuplicate,
|
||||
handleManageRules,
|
||||
}: {
|
||||
exceptionsList: ExceptionListInfo;
|
||||
|
@ -48,13 +57,20 @@ export const useExceptionsListCard = ({
|
|||
includeExpiredExceptions,
|
||||
}: ExportListAction) => () => Promise<void>;
|
||||
handleDelete: ({ id, listId, namespaceType }: ListAction) => () => Promise<void>;
|
||||
handleDuplicate: ({
|
||||
listId,
|
||||
name,
|
||||
namespaceType,
|
||||
includeExpiredExceptions,
|
||||
}: DuplicateListAction) => () => Promise<void>;
|
||||
handleManageRules: () => void;
|
||||
}) => {
|
||||
const [viewerStatus, setViewerStatus] = useState<ViewerStatus | string>(ViewerStatus.LOADING);
|
||||
const [exceptionToEdit, setExceptionToEdit] = useState<ExceptionListItemSchema>();
|
||||
const [showAddExceptionFlyout, setShowAddExceptionFlyout] = useState(false);
|
||||
const [showEditExceptionFlyout, setShowEditExceptionFlyout] = useState(false);
|
||||
const [showExportModal, setShowExportModal] = useState(false);
|
||||
const [showIncludeExpiredExceptionsModal, setShowIncludeExpiredExceptionsModal] =
|
||||
useState<CheckExceptionTtlActionTypes | null>(null);
|
||||
|
||||
const {
|
||||
name: listName,
|
||||
|
@ -135,10 +151,19 @@ export const useExceptionsListCard = ({
|
|||
includeExpiredExceptions: true,
|
||||
})();
|
||||
} else {
|
||||
setShowExportModal(true);
|
||||
setShowIncludeExpiredExceptionsModal(CHECK_EXCEPTION_TTL_ACTION_TYPES.EXPORT);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Duplicate',
|
||||
icon: 'copy',
|
||||
label: i18n.DUPLICATE_EXCEPTION_LIST,
|
||||
disabled: listCannotBeEdited,
|
||||
onClick: (_: React.MouseEvent<Element, MouseEvent>) => {
|
||||
setShowIncludeExpiredExceptionsModal(CHECK_EXCEPTION_TTL_ACTION_TYPES.DUPLICATE);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Delete',
|
||||
icon: 'trash',
|
||||
|
@ -163,16 +188,15 @@ export const useExceptionsListCard = ({
|
|||
},
|
||||
],
|
||||
[
|
||||
listCannotBeEdited,
|
||||
listType,
|
||||
handleExport,
|
||||
exceptionsList.id,
|
||||
exceptionsList.list_id,
|
||||
exceptionsList.name,
|
||||
exceptionsList.namespace_type,
|
||||
handleDelete,
|
||||
setShowExportModal,
|
||||
listCannotBeEdited,
|
||||
handleManageRules,
|
||||
handleExport,
|
||||
listType,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -197,24 +221,42 @@ export const useExceptionsListCard = ({
|
|||
);
|
||||
|
||||
const onExportListClick = useCallback(() => {
|
||||
setShowExportModal(true);
|
||||
}, [setShowExportModal]);
|
||||
setShowIncludeExpiredExceptionsModal(CHECK_EXCEPTION_TTL_ACTION_TYPES.EXPORT);
|
||||
}, [setShowIncludeExpiredExceptionsModal]);
|
||||
|
||||
const handleCancelExportModal = () => {
|
||||
setShowExportModal(false);
|
||||
const handleCancelExpiredExceptionsModal = () => {
|
||||
setShowIncludeExpiredExceptionsModal(null);
|
||||
};
|
||||
|
||||
const handleConfirmExportModal = useCallback(
|
||||
const handleConfirmExpiredExceptionsModal = useCallback(
|
||||
(includeExpiredExceptions: boolean): void => {
|
||||
handleExport({
|
||||
id: exceptionsList.id,
|
||||
listId: exceptionsList.list_id,
|
||||
name: exceptionsList.name,
|
||||
namespaceType: exceptionsList.namespace_type,
|
||||
includeExpiredExceptions,
|
||||
})();
|
||||
if (showIncludeExpiredExceptionsModal === CHECK_EXCEPTION_TTL_ACTION_TYPES.EXPORT) {
|
||||
handleExport({
|
||||
id: exceptionsList.id,
|
||||
listId: exceptionsList.list_id,
|
||||
name: exceptionsList.name,
|
||||
namespaceType: exceptionsList.namespace_type,
|
||||
includeExpiredExceptions,
|
||||
})();
|
||||
}
|
||||
if (showIncludeExpiredExceptionsModal === CHECK_EXCEPTION_TTL_ACTION_TYPES.DUPLICATE) {
|
||||
handleDuplicate({
|
||||
listId: exceptionsList.list_id,
|
||||
name: exceptionsList.name,
|
||||
namespaceType: exceptionsList.namespace_type,
|
||||
includeExpiredExceptions,
|
||||
})();
|
||||
}
|
||||
},
|
||||
[handleExport, exceptionsList]
|
||||
[
|
||||
showIncludeExpiredExceptionsModal,
|
||||
handleExport,
|
||||
exceptionsList.id,
|
||||
exceptionsList.list_id,
|
||||
exceptionsList.name,
|
||||
exceptionsList.namespace_type,
|
||||
handleDuplicate,
|
||||
]
|
||||
);
|
||||
|
||||
// routes to x-pack/plugins/security_solution/public/exceptions/routes.tsx
|
||||
|
@ -255,9 +297,9 @@ export const useExceptionsListCard = ({
|
|||
emptyViewerTitle,
|
||||
emptyViewerBody,
|
||||
emptyViewerButtonText,
|
||||
showExportModal,
|
||||
showIncludeExpiredExceptionsModal,
|
||||
onExportListClick,
|
||||
handleCancelExportModal,
|
||||
handleConfirmExportModal,
|
||||
handleCancelExpiredExceptionsModal,
|
||||
handleConfirmExpiredExceptionsModal,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -51,7 +51,7 @@ export const useListDetailsView = (exceptionListId: string) => {
|
|||
const { http, notifications } = services;
|
||||
const { navigateToApp } = services.application;
|
||||
|
||||
const { exportExceptionList, deleteExceptionList } = useApi(http);
|
||||
const { exportExceptionList, deleteExceptionList, duplicateExceptionList } = useApi(http);
|
||||
|
||||
const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData();
|
||||
|
||||
|
@ -190,6 +190,35 @@ export const useListDetailsView = (exceptionListId: string) => {
|
|||
[list, exportExceptionList, handleErrorStatus, toasts]
|
||||
);
|
||||
|
||||
const onDuplicateList = useCallback(
|
||||
async (includeExpiredExceptions: boolean) => {
|
||||
try {
|
||||
if (!list) return;
|
||||
await duplicateExceptionList({
|
||||
listId: list.list_id,
|
||||
includeExpiredExceptions,
|
||||
namespaceType: list.namespace_type,
|
||||
onError: (error: Error) => handleErrorStatus(error),
|
||||
onSuccess: (newList: ExceptionListSchema) => {
|
||||
toasts?.addSuccess(i18n.EXCEPTION_LIST_DUPLICATED_SUCCESSFULLY(list.name));
|
||||
navigateToApp(APP_UI_ID, {
|
||||
deepLinkId: SecurityPageName.exceptions,
|
||||
path: `/details/${newList.list_id}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
handleErrorStatus(
|
||||
error,
|
||||
undefined,
|
||||
i18n.EXCEPTION_DUPLICATE_ERROR,
|
||||
i18n.EXCEPTION_DUPLICATE_ERROR_DESCRIPTION
|
||||
);
|
||||
}
|
||||
},
|
||||
[list, duplicateExceptionList, handleErrorStatus, toasts, navigateToApp]
|
||||
);
|
||||
|
||||
const handleOnDownload = useCallback(() => {
|
||||
setExportedList(undefined);
|
||||
}, []);
|
||||
|
@ -384,6 +413,7 @@ export const useListDetailsView = (exceptionListId: string) => {
|
|||
refreshExceptions,
|
||||
disableManageButton,
|
||||
handleDelete,
|
||||
onDuplicateList,
|
||||
onEditListDetails,
|
||||
onExportList,
|
||||
onDeleteList,
|
||||
|
|
|
@ -25,7 +25,8 @@ import { AutoDownload } from '../../../common/components/auto_download/auto_down
|
|||
import { ListWithSearch, ManageRules, ListDetailsLinkAnchor } from '../../components';
|
||||
import { useListDetailsView } from '../../hooks';
|
||||
import * as i18n from '../../translations';
|
||||
import { ExportExceptionsListModal } from '../../components/export_exceptions_list_modal';
|
||||
import type { CheckExceptionTtlActionTypes } from '../../components/expired_exceptions_list_items_modal';
|
||||
import { IncludeExpiredExceptionsModal } from '../../components/expired_exceptions_list_items_modal';
|
||||
|
||||
export const ListsDetailViewComponent: FC = () => {
|
||||
const { detailName: exceptionListId } = useParams<{
|
||||
|
@ -52,6 +53,7 @@ export const ListsDetailViewComponent: FC = () => {
|
|||
refreshExceptions,
|
||||
disableManageButton,
|
||||
onEditListDetails,
|
||||
onDuplicateList,
|
||||
onExportList,
|
||||
onManageRules,
|
||||
onSaveManageRules,
|
||||
|
@ -62,20 +64,33 @@ export const ListsDetailViewComponent: FC = () => {
|
|||
handleReferenceDelete,
|
||||
} = useListDetailsView(exceptionListId);
|
||||
|
||||
const [showExportModal, setShowExportModal] = useState(false);
|
||||
const [showIncludeExpiredExceptionItemsModal, setShowIncludeExpiredExceptionItemsModal] =
|
||||
useState<CheckExceptionTtlActionTypes | null>(null);
|
||||
|
||||
const onModalClose = useCallback(() => setShowExportModal(false), [setShowExportModal]);
|
||||
const onModalClose = useCallback(
|
||||
() => setShowIncludeExpiredExceptionItemsModal(null),
|
||||
[setShowIncludeExpiredExceptionItemsModal]
|
||||
);
|
||||
|
||||
const onModalOpen = useCallback(() => setShowExportModal(true), [setShowExportModal]);
|
||||
const onModalOpen = useCallback(
|
||||
(actionType: CheckExceptionTtlActionTypes) => {
|
||||
setShowIncludeExpiredExceptionItemsModal(actionType);
|
||||
},
|
||||
[setShowIncludeExpiredExceptionItemsModal]
|
||||
);
|
||||
|
||||
const handleExportList = useCallback(() => {
|
||||
if (list?.type === ExceptionListTypeEnum.ENDPOINT) {
|
||||
onExportList(true);
|
||||
} else {
|
||||
onModalOpen();
|
||||
onModalOpen('export');
|
||||
}
|
||||
}, [onModalOpen, list, onExportList]);
|
||||
|
||||
const handleDuplicateList = useCallback(() => {
|
||||
onModalOpen('duplicate');
|
||||
}, [onModalOpen]);
|
||||
|
||||
const detailsViewContent = useMemo(() => {
|
||||
if (viewerStatus === ViewerStatus.ERROR)
|
||||
return <EmptyViewerState isReadOnly={isReadOnly} viewerStatus={viewerStatus} />;
|
||||
|
@ -99,6 +114,7 @@ export const ListsDetailViewComponent: FC = () => {
|
|||
onExportList={handleExportList}
|
||||
onDeleteList={handleDelete}
|
||||
onManageRules={onManageRules}
|
||||
onDuplicateList={handleDuplicateList}
|
||||
dataTestSubj="exceptionListManagement"
|
||||
/>
|
||||
|
||||
|
@ -125,47 +141,52 @@ export const ListsDetailViewComponent: FC = () => {
|
|||
onRuleSelectionChange={onRuleSelectionChange}
|
||||
/>
|
||||
) : null}
|
||||
{showExportModal && (
|
||||
<ExportExceptionsListModal
|
||||
onModalConfirm={onExportList}
|
||||
{showIncludeExpiredExceptionItemsModal && (
|
||||
<IncludeExpiredExceptionsModal
|
||||
onModalConfirm={
|
||||
showIncludeExpiredExceptionItemsModal === 'export' ? onExportList : onDuplicateList
|
||||
}
|
||||
handleCloseModal={onModalClose}
|
||||
action={showIncludeExpiredExceptionItemsModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
canUserEditList,
|
||||
disableManageButton,
|
||||
exportedList,
|
||||
handleOnDownload,
|
||||
headerBackOptions,
|
||||
invalidListId,
|
||||
isLoading,
|
||||
viewerStatus,
|
||||
isReadOnly,
|
||||
linkedRules,
|
||||
isLoading,
|
||||
invalidListId,
|
||||
listName,
|
||||
list,
|
||||
listDescription,
|
||||
listId,
|
||||
listName,
|
||||
linkedRules,
|
||||
canUserEditList,
|
||||
headerBackOptions,
|
||||
onEditListDetails,
|
||||
handleExportList,
|
||||
handleDelete,
|
||||
onManageRules,
|
||||
handleDuplicateList,
|
||||
exportedList,
|
||||
handleOnDownload,
|
||||
refreshExceptions,
|
||||
referenceModalState.contentText,
|
||||
referenceModalState.rulesReferences,
|
||||
refreshExceptions,
|
||||
showManageButtonLoader,
|
||||
showManageRulesFlyout,
|
||||
showReferenceErrorModal,
|
||||
showExportModal,
|
||||
viewerStatus,
|
||||
onCancelManageRules,
|
||||
onEditListDetails,
|
||||
onExportList,
|
||||
onManageRules,
|
||||
onRuleSelectionChange,
|
||||
onSaveManageRules,
|
||||
handleCloseReferenceErrorModal,
|
||||
handleDelete,
|
||||
handleReferenceDelete,
|
||||
showReferenceErrorModal,
|
||||
showManageRulesFlyout,
|
||||
showManageButtonLoader,
|
||||
disableManageButton,
|
||||
onSaveManageRules,
|
||||
onCancelManageRules,
|
||||
onRuleSelectionChange,
|
||||
showIncludeExpiredExceptionItemsModal,
|
||||
onExportList,
|
||||
onDuplicateList,
|
||||
onModalClose,
|
||||
handleExportList,
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -93,7 +93,7 @@ export const SharedLists = React.memo(() => {
|
|||
application: { navigateToApp },
|
||||
},
|
||||
} = useKibana();
|
||||
const { exportExceptionList, deleteExceptionList } = useApi(http);
|
||||
const { exportExceptionList, deleteExceptionList, duplicateExceptionList } = useApi(http);
|
||||
|
||||
const [showReferenceErrorModal, setShowReferenceErrorModal] = useState(false);
|
||||
const [referenceModalState, setReferenceModalState] = useState<ReferenceModalState>(
|
||||
|
@ -262,6 +262,45 @@ export const SharedLists = React.memo(() => {
|
|||
[]
|
||||
);
|
||||
|
||||
const handleDuplicationError = useCallback(
|
||||
(err: Error) => {
|
||||
addError(err, { title: i18n.EXCEPTION_DUPLICATE_ERROR });
|
||||
},
|
||||
[addError]
|
||||
);
|
||||
|
||||
const handleDuplicateSuccess = useCallback(
|
||||
(name: string) => (): void => {
|
||||
addSuccess(i18n.EXCEPTION_LIST_DUPLICATED_SUCCESSFULLY(name));
|
||||
handleRefresh();
|
||||
},
|
||||
[addSuccess, handleRefresh]
|
||||
);
|
||||
|
||||
const handleDuplicate = useCallback(
|
||||
({
|
||||
listId,
|
||||
name,
|
||||
namespaceType,
|
||||
includeExpiredExceptions,
|
||||
}: {
|
||||
listId: string;
|
||||
name: string;
|
||||
namespaceType: NamespaceType;
|
||||
includeExpiredExceptions: boolean;
|
||||
}) =>
|
||||
async () => {
|
||||
await duplicateExceptionList({
|
||||
includeExpiredExceptions,
|
||||
listId,
|
||||
namespaceType,
|
||||
onError: handleDuplicationError,
|
||||
onSuccess: handleDuplicateSuccess(name),
|
||||
});
|
||||
},
|
||||
[duplicateExceptionList, handleDuplicateSuccess, handleDuplicationError]
|
||||
);
|
||||
|
||||
const handleCloseReferenceErrorModal = useCallback((): void => {
|
||||
setShowReferenceErrorModal(false);
|
||||
setReferenceModalState({
|
||||
|
@ -546,6 +585,7 @@ export const SharedLists = React.memo(() => {
|
|||
exceptionsList={excList}
|
||||
handleDelete={handleDelete}
|
||||
handleExport={handleExport}
|
||||
handleDuplicate={handleDuplicate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -167,3 +167,17 @@ export const EXCEPTION_EXPORT_ERROR_DESCRIPTION = i18n.translate(
|
|||
defaultMessage: 'An error occurred exporting a list',
|
||||
}
|
||||
);
|
||||
|
||||
export const DUPLICATE_EXCEPTION_LIST = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.duplicateExceptionList',
|
||||
{
|
||||
defaultMessage: 'Duplicate exception list',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_DUPLICATE_ERROR_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.exceptionsTable.duplicateListDescription',
|
||||
{
|
||||
defaultMessage: 'An error occurred duplicating a list',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -125,6 +125,19 @@ export const EXCEPTION_EXPORT_ERROR = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_LIST_DUPLICATED_SUCCESSFULLY = (listName: string) =>
|
||||
i18n.translate('xpack.securitySolution.exceptions.list.duplicate_success', {
|
||||
values: { listName },
|
||||
defaultMessage: 'Exception list "{listName}" duplicated successfully',
|
||||
});
|
||||
|
||||
export const EXCEPTION_DUPLICATE_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.duplicateError',
|
||||
{
|
||||
defaultMessage: 'Exception list duplication error',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_DELETE_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.all.exceptions.deleteError',
|
||||
{
|
||||
|
@ -371,29 +384,59 @@ export const SORT_BY_CREATE_AT = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const EXPORT_MODAL_CANCEL_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exportModalCancelButton',
|
||||
export const EXPIRED_EXCEPTIONS_MODAL_CANCEL_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.expiredExceptionModalCancelButton',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPORT_MODAL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exportModalTitle',
|
||||
export const EXPIRED_EXCEPTIONS_MODAL_EXPORT_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.expiredExceptionModalExportTitle',
|
||||
{
|
||||
defaultMessage: 'Export exception list',
|
||||
defaultMessage: 'Export exception list?',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPORT_MODAL_INCLUDE_SWITCH_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exportModalIncludeSwitchLabel',
|
||||
export const EXPIRED_EXCEPTIONS_MODAL_DUPLICATE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.expiredExceptionModalDuplicateTitle',
|
||||
{
|
||||
defaultMessage: 'Duplicate exception list?',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPIRED_EXCEPTIONS_MODAL_DUPLICATE_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.expiredExceptionModalIncludeDuplicateDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'You’re duplicating an exception list. Switch the toggle off to exclude expired exceptions.',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPIRED_EXCEPTIONS_MODAL_EXPORT_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.expiredExceptionModalIncludeExportDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'You’re exporting an exception list. Switch the toggle off to exclude expired exceptions.',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPIRED_EXCEPTIONS_MODAL_INCLUDE_SWITCH_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.expiredExceptionModalIncludeSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Include expired exceptions',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPORT_MODAL_CONFIRM_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.exportModalConfirmButton',
|
||||
export const EXPIRED_EXCEPTIONS_MODAL_CONFIRM_DUPLICATE_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.expiredExceptionModalConfirmDuplicateButton',
|
||||
{
|
||||
defaultMessage: 'Duplicate',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPIRED_EXCEPTIONS_MODAL_CONFIRM_EXPORT_BUTTON = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.expiredExceptionModalConfirmExportButton',
|
||||
{
|
||||
defaultMessage: 'Export',
|
||||
}
|
||||
|
|
|
@ -432,8 +432,10 @@ export const performBulkActionRoute = (
|
|||
}
|
||||
|
||||
let shouldDuplicateExceptions = true;
|
||||
let shouldDuplicateExpiredExceptions = true;
|
||||
if (body.duplicate !== undefined) {
|
||||
shouldDuplicateExceptions = body.duplicate.include_exceptions;
|
||||
shouldDuplicateExpiredExceptions = body.duplicate.include_expired_exceptions;
|
||||
}
|
||||
|
||||
const duplicateRuleToCreate = await duplicateRule({
|
||||
|
@ -449,6 +451,7 @@ export const performBulkActionRoute = (
|
|||
? await duplicateExceptions({
|
||||
ruleId: rule.params.ruleId,
|
||||
exceptionLists: rule.params.exceptionsList,
|
||||
includeExpiredExceptions: shouldDuplicateExpiredExceptions,
|
||||
exceptionsClient,
|
||||
})
|
||||
: [];
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* 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 { duplicateExceptions } from './duplicate_exceptions';
|
||||
import { getExceptionListClientMock } from '@kbn/lists-plugin/server/services/exception_lists/exception_list_client.mock';
|
||||
import type { List } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { ExceptionListClient } from '@kbn/lists-plugin/server';
|
||||
import { getDetectionsExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('duplicateExceptions', () => {
|
||||
let exceptionsClient: ExceptionListClient;
|
||||
|
||||
beforeAll(() => {
|
||||
exceptionsClient = getExceptionListClientMock();
|
||||
exceptionsClient.duplicateExceptionListAndItems = jest.fn();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns empty array if no exceptions to duplicate', async () => {
|
||||
const result = await duplicateExceptions({
|
||||
ruleId: 'rule_123',
|
||||
exceptionLists: [],
|
||||
exceptionsClient,
|
||||
includeExpiredExceptions: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns array referencing the same shared exception lists if no rule default exceptions included', async () => {
|
||||
const sharedExceptionListReference: List = {
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
list_id: 'my_list',
|
||||
namespace_type: 'single',
|
||||
id: '1234',
|
||||
};
|
||||
|
||||
const result = await duplicateExceptions({
|
||||
ruleId: 'rule_123',
|
||||
exceptionLists: [sharedExceptionListReference],
|
||||
exceptionsClient,
|
||||
includeExpiredExceptions: false,
|
||||
});
|
||||
|
||||
expect(exceptionsClient.duplicateExceptionListAndItems).not.toHaveBeenCalled();
|
||||
expect(result).toEqual([sharedExceptionListReference]);
|
||||
});
|
||||
|
||||
it('duplicates rule default and shared exceptions', async () => {
|
||||
const newDefaultRuleList = {
|
||||
...getDetectionsExceptionListSchemaMock(),
|
||||
type: ExceptionListTypeEnum.RULE_DEFAULT,
|
||||
list_id: 'rule_default_list_dupe',
|
||||
namespace_type: 'single',
|
||||
id: '123-abc',
|
||||
};
|
||||
|
||||
exceptionsClient.getExceptionList = jest.fn().mockResolvedValue({
|
||||
...getDetectionsExceptionListSchemaMock(),
|
||||
type: ExceptionListTypeEnum.RULE_DEFAULT,
|
||||
list_id: 'rule_default_list',
|
||||
namespace_type: 'single',
|
||||
id: '5678',
|
||||
});
|
||||
exceptionsClient.duplicateExceptionListAndItems = jest
|
||||
.fn()
|
||||
.mockResolvedValue(newDefaultRuleList);
|
||||
|
||||
const sharedExceptionListReference: List = {
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
list_id: 'my_list',
|
||||
namespace_type: 'single',
|
||||
id: '1234',
|
||||
};
|
||||
|
||||
const ruleDefaultListReference: List = {
|
||||
type: ExceptionListTypeEnum.RULE_DEFAULT,
|
||||
list_id: 'rule_default_list',
|
||||
namespace_type: 'single',
|
||||
id: '5678',
|
||||
};
|
||||
|
||||
const result = await duplicateExceptions({
|
||||
ruleId: 'rule_123',
|
||||
exceptionLists: [sharedExceptionListReference, ruleDefaultListReference],
|
||||
exceptionsClient,
|
||||
includeExpiredExceptions: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
sharedExceptionListReference,
|
||||
{
|
||||
type: newDefaultRuleList.type,
|
||||
namespace_type: newDefaultRuleList.namespace_type,
|
||||
id: newDefaultRuleList.id,
|
||||
list_id: newDefaultRuleList.list_id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('throws error if rule default list to duplicate not found', async () => {
|
||||
exceptionsClient.getExceptionList = jest.fn().mockResolvedValue(null);
|
||||
|
||||
const ruleDefaultListReference: List = {
|
||||
type: ExceptionListTypeEnum.RULE_DEFAULT,
|
||||
list_id: 'my_list',
|
||||
namespace_type: 'single',
|
||||
id: '1234',
|
||||
};
|
||||
|
||||
await expect(() =>
|
||||
duplicateExceptions({
|
||||
ruleId: 'rule_123',
|
||||
exceptionLists: [ruleDefaultListReference],
|
||||
exceptionsClient,
|
||||
includeExpiredExceptions: false,
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unable to duplicate rule default exceptions - unable to find their container with list_id: "my_list"]`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error if list duplication returns null', async () => {
|
||||
exceptionsClient.getExceptionList = jest.fn().mockResolvedValue({
|
||||
...getDetectionsExceptionListSchemaMock(),
|
||||
type: ExceptionListTypeEnum.RULE_DEFAULT,
|
||||
list_id: 'my_list',
|
||||
namespace_type: 'single',
|
||||
id: '1234',
|
||||
});
|
||||
exceptionsClient.duplicateExceptionListAndItems = jest.fn().mockResolvedValue(null);
|
||||
|
||||
const ruleDefaultListReference: List = {
|
||||
type: ExceptionListTypeEnum.RULE_DEFAULT,
|
||||
list_id: 'my_list',
|
||||
namespace_type: 'single',
|
||||
id: '1234',
|
||||
};
|
||||
|
||||
await expect(() =>
|
||||
duplicateExceptions({
|
||||
ruleId: 'rule_123',
|
||||
exceptionLists: [ruleDefaultListReference],
|
||||
exceptionsClient,
|
||||
includeExpiredExceptions: false,
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unable to duplicate rule default exception items for rule_id: rule_123]`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -5,23 +5,42 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { ExceptionListClient } from '@kbn/lists-plugin/server';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import type { RuleParams } from '../../../rule_schema';
|
||||
|
||||
const ERROR_DUPLICATING = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.cloneExceptions.errorDuplicatingList',
|
||||
{
|
||||
defaultMessage:
|
||||
'Unable to duplicate rule default exceptions - unable to find their container with list_id:',
|
||||
}
|
||||
);
|
||||
|
||||
const ERROR_DUPLICATING_ITEMS = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.cloneExceptions.errorDuplicatingListItems',
|
||||
{
|
||||
defaultMessage: 'Unable to duplicate rule default exception items for rule_id:',
|
||||
}
|
||||
);
|
||||
|
||||
interface DuplicateExceptionsParams {
|
||||
ruleId: RuleParams['ruleId'];
|
||||
exceptionLists: RuleParams['exceptionsList'];
|
||||
exceptionsClient: ExceptionListClient | undefined;
|
||||
includeExpiredExceptions: boolean;
|
||||
}
|
||||
|
||||
export const duplicateExceptions = async ({
|
||||
ruleId,
|
||||
exceptionLists,
|
||||
exceptionsClient,
|
||||
includeExpiredExceptions,
|
||||
}: DuplicateExceptionsParams): Promise<RuleParams['exceptionsList']> => {
|
||||
if (exceptionLists == null) {
|
||||
if (exceptionLists == null || !exceptionLists.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -37,24 +56,36 @@ export const duplicateExceptions = async ({
|
|||
// 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({
|
||||
// fetch list container
|
||||
const listToDuplicate = await exceptionsClient.getExceptionList({
|
||||
id: undefined,
|
||||
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}`);
|
||||
}
|
||||
if (listToDuplicate == null) {
|
||||
throw new Error(`${ERROR_DUPLICATING} "${ruleDefaultList.list_id}"`);
|
||||
} else {
|
||||
const ruleDefaultExceptionList = await exceptionsClient.duplicateExceptionListAndItems({
|
||||
list: listToDuplicate,
|
||||
namespaceType: ruleDefaultList.namespace_type,
|
||||
includeExpiredExceptions,
|
||||
});
|
||||
|
||||
return [
|
||||
...sharedLists,
|
||||
{
|
||||
id: ruleDefaultExceptionList.id,
|
||||
list_id: ruleDefaultExceptionList.list_id,
|
||||
namespace_type: ruleDefaultExceptionList.namespace_type,
|
||||
type: ruleDefaultExceptionList.type,
|
||||
},
|
||||
];
|
||||
if (ruleDefaultExceptionList == null) {
|
||||
throw new Error(`${ERROR_DUPLICATING_ITEMS} ${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
|
||||
|
|
|
@ -27824,7 +27824,6 @@
|
|||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.disable.successToastDescription": "Désactivation réussie de {totalRules, plural, =1 {{totalRules} règle} other {{totalRules} règles}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.errorToastDescription": "Impossible de dupliquer {rulesCount, plural, =1 {# règle} other {# règles}}.",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalBody": "Vous dupliquez {rulesCount, plural, one {# règle sélectionnée} other {# règles sélectionnées}}. Veuillez choisir comment dupliquer les exceptions existantes",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalTitle": "Dupliquer {rulesCount, plural, one {la règle} other {les règles}} avec les exceptions ?",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.with": "Dupliquer {rulesCount, plural, one {la règle et ses} other {les règles et leurs}} exceptions",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.without": "Dupliquer uniquement {rulesCount, plural, one {la règle} other {les règles}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "Duplication réussie de {totalRules, plural, =1 {{totalRules} règle} other {{totalRules} règles}}",
|
||||
|
@ -30891,10 +30890,6 @@
|
|||
"xpack.securitySolution.exceptions.exceptionListsCloseImportFlyout": "Fermer",
|
||||
"xpack.securitySolution.exceptions.exceptionListsFilePickerPrompt": "Sélectionner ou glisser-déposer plusieurs fichiers",
|
||||
"xpack.securitySolution.exceptions.exceptionListsImportButton": "Importer la liste",
|
||||
"xpack.securitySolution.exceptions.exportModalCancelButton": "Annuler",
|
||||
"xpack.securitySolution.exceptions.exportModalConfirmButton": "Exporter",
|
||||
"xpack.securitySolution.exceptions.exportModalIncludeSwitchLabel": "Inclure les exceptions ayant expiré",
|
||||
"xpack.securitySolution.exceptions.exportModalTitle": "Exporter la liste d'exceptions",
|
||||
"xpack.securitySolution.exceptions.fetchError": "Erreur lors de la récupération de la liste d'exceptions",
|
||||
"xpack.securitySolution.exceptions.fetchingReferencesErrorToastTitle": "Erreur lors de la récupération des références d'exceptions",
|
||||
"xpack.securitySolution.exceptions.list.exception.item.card.delete.label": "Supprimer une exception à une règle",
|
||||
|
|
|
@ -27804,7 +27804,6 @@
|
|||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.disable.successToastDescription": "{totalRules, plural, =1 {{totalRules}個のルール} other {{totalRules}個のルール}}が正常に無効にされました",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.errorToastDescription": "{rulesCount, plural, =1 {#個のルール} other {#個のルール}}を複製できませんでした。",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalBody": "{rulesCount, plural, other {#個の選択したルール}}を複製しています。既存の例外を複製する方法を選択してください",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalTitle": "{rulesCount, plural, other {ルール}}と例外を複製しますか?",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.with": "{rulesCount, plural, other {ルール}}と例外を複製",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.without": "{rulesCount, plural, other {ルール}}のみを複製",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "{totalRules, plural, =1 {{totalRules}個のルール} other {{totalRules}個のルール}}が正常に複製されました",
|
||||
|
@ -30870,10 +30869,6 @@
|
|||
"xpack.securitySolution.exceptions.exceptionListsCloseImportFlyout": "閉じる",
|
||||
"xpack.securitySolution.exceptions.exceptionListsFilePickerPrompt": "複数のファイルを選択するかドラッグしてください",
|
||||
"xpack.securitySolution.exceptions.exceptionListsImportButton": "リストをインポート",
|
||||
"xpack.securitySolution.exceptions.exportModalCancelButton": "キャンセル",
|
||||
"xpack.securitySolution.exceptions.exportModalConfirmButton": "エクスポート",
|
||||
"xpack.securitySolution.exceptions.exportModalIncludeSwitchLabel": "有効期限切れの例外を含める",
|
||||
"xpack.securitySolution.exceptions.exportModalTitle": "例外リストのエクスポート",
|
||||
"xpack.securitySolution.exceptions.fetchError": "例外リストの取得エラー",
|
||||
"xpack.securitySolution.exceptions.fetchingReferencesErrorToastTitle": "例外参照の取得エラー",
|
||||
"xpack.securitySolution.exceptions.list.exception.item.card.delete.label": "ルール例外の削除",
|
||||
|
|
|
@ -27819,7 +27819,6 @@
|
|||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.disable.successToastDescription": "已成功禁用 {totalRules, plural, other {{totalRules} 个规则}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.errorToastDescription": "无法复制 {rulesCount, plural, other {# 个规则}}。",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalBody": "您正在复制 {rulesCount, plural, other {# 个选定规则}},请选择您希望如何复制现有例外",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.modalTitle": "复制存在例外的{rulesCount, plural, other {规则}}?",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.with": "复制{rulesCount, plural, other {规则}}及其例外",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.without": "仅复制{rulesCount, plural, other {规则}}",
|
||||
"xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.successToastDescription": "已成功复制 {totalRules, plural, other {{totalRules} 个规则}}",
|
||||
|
@ -30886,10 +30885,6 @@
|
|||
"xpack.securitySolution.exceptions.exceptionListsCloseImportFlyout": "关闭",
|
||||
"xpack.securitySolution.exceptions.exceptionListsFilePickerPrompt": "选择或拖放多个文件",
|
||||
"xpack.securitySolution.exceptions.exceptionListsImportButton": "导入列表",
|
||||
"xpack.securitySolution.exceptions.exportModalCancelButton": "取消",
|
||||
"xpack.securitySolution.exceptions.exportModalConfirmButton": "导出",
|
||||
"xpack.securitySolution.exceptions.exportModalIncludeSwitchLabel": "包括已过期例外",
|
||||
"xpack.securitySolution.exceptions.exportModalTitle": "导出例外列表",
|
||||
"xpack.securitySolution.exceptions.fetchError": "提取例外列表时出错",
|
||||
"xpack.securitySolution.exceptions.fetchingReferencesErrorToastTitle": "提取例外引用时出错",
|
||||
"xpack.securitySolution.exceptions.list.exception.item.card.delete.label": "删除规则例外",
|
||||
|
|
|
@ -17,7 +17,10 @@ import {
|
|||
BulkActionType,
|
||||
BulkActionEditType,
|
||||
} from '@kbn/security-solution-plugin/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { getCreateExceptionListDetectionSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock';
|
||||
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
|
||||
import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock';
|
||||
import { deleteAllExceptions } from '../../../lists_api_integration/utils';
|
||||
import {
|
||||
binaryToString,
|
||||
createLegacyRuleAction,
|
||||
|
@ -35,6 +38,7 @@ import {
|
|||
removeServerGeneratedProperties,
|
||||
waitForRuleSuccess,
|
||||
} from '../../utils';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
|
@ -416,7 +420,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
.send({
|
||||
query: '',
|
||||
action: BulkActionType.duplicate,
|
||||
duplicate: { include_exceptions: false },
|
||||
duplicate: { include_exceptions: false, include_expired_exceptions: false },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
@ -434,6 +438,216 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(rulesResponse.total).to.eql(2);
|
||||
});
|
||||
|
||||
it('should duplicate rules with exceptions - expired exceptions included', async () => {
|
||||
await deleteAllExceptions(supertest, log);
|
||||
|
||||
const expiredDate = new Date(Date.now() - 1000000).toISOString();
|
||||
|
||||
// create an exception list
|
||||
const { body: exceptionList } = await supertest
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getCreateExceptionListDetectionSchemaMock())
|
||||
.expect(200);
|
||||
// create an exception list item
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_ITEM_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ ...getCreateExceptionListItemMinimalSchemaMock(), expire_time: expiredDate })
|
||||
.expect(200);
|
||||
|
||||
const ruleId = 'ruleId';
|
||||
const ruleToDuplicate = {
|
||||
...getSimpleRule(ruleId),
|
||||
exceptions_list: [
|
||||
{
|
||||
type: exceptionList.type,
|
||||
list_id: exceptionList.list_id,
|
||||
id: exceptionList.id,
|
||||
namespace_type: exceptionList.namespace_type,
|
||||
},
|
||||
],
|
||||
};
|
||||
const newRule = await createRule(supertest, log, ruleToDuplicate);
|
||||
|
||||
// add an exception item to the rule
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/${newRule.id}/exceptions`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
description: 'Exception item for rule default exception list',
|
||||
entries: [
|
||||
{
|
||||
field: 'some.not.nested.field',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'some value',
|
||||
},
|
||||
],
|
||||
name: 'Sample exception item',
|
||||
type: 'simple',
|
||||
expire_time: expiredDate,
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body } = await postBulkAction()
|
||||
.send({
|
||||
query: '',
|
||||
action: BulkActionType.duplicate,
|
||||
duplicate: { include_exceptions: true, include_expired_exceptions: true },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: foundItems } = await supertest
|
||||
.get(
|
||||
`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${body.attributes.results.created[0].exceptions_list[1].list_id}`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
// Item should have been duplicated, even if expired
|
||||
expect(foundItems.total).to.eql(1);
|
||||
|
||||
expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 });
|
||||
|
||||
// Check that the duplicated rule is returned with the response
|
||||
expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`);
|
||||
|
||||
// Check that the exceptions are duplicated
|
||||
expect(body.attributes.results.created[0].exceptions_list).to.eql([
|
||||
{
|
||||
type: exceptionList.type,
|
||||
list_id: exceptionList.list_id,
|
||||
id: exceptionList.id,
|
||||
namespace_type: exceptionList.namespace_type,
|
||||
},
|
||||
{
|
||||
id: body.attributes.results.created[0].exceptions_list[1].id,
|
||||
list_id: body.attributes.results.created[0].exceptions_list[1].list_id,
|
||||
namespace_type: 'single',
|
||||
type: 'rule_default',
|
||||
},
|
||||
]);
|
||||
|
||||
// Check that the updates have been persisted
|
||||
const { body: rulesResponse } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}/_find`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
|
||||
expect(rulesResponse.total).to.eql(2);
|
||||
});
|
||||
|
||||
it('should duplicate rules with exceptions - expired exceptions excluded', async () => {
|
||||
await deleteAllExceptions(supertest, log);
|
||||
|
||||
const expiredDate = new Date(Date.now() - 1000000).toISOString();
|
||||
|
||||
// create an exception list
|
||||
const { body: exceptionList } = await supertest
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getCreateExceptionListDetectionSchemaMock())
|
||||
.expect(200);
|
||||
// create an exception list item
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_ITEM_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ ...getCreateExceptionListItemMinimalSchemaMock(), expire_time: expiredDate })
|
||||
.expect(200);
|
||||
|
||||
const ruleId = 'ruleId';
|
||||
const ruleToDuplicate = {
|
||||
...getSimpleRule(ruleId),
|
||||
exceptions_list: [
|
||||
{
|
||||
type: exceptionList.type,
|
||||
list_id: exceptionList.list_id,
|
||||
id: exceptionList.id,
|
||||
namespace_type: exceptionList.namespace_type,
|
||||
},
|
||||
],
|
||||
};
|
||||
const newRule = await createRule(supertest, log, ruleToDuplicate);
|
||||
|
||||
// add an exception item to the rule
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/${newRule.id}/exceptions`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
description: 'Exception item for rule default exception list',
|
||||
entries: [
|
||||
{
|
||||
field: 'some.not.nested.field',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'some value',
|
||||
},
|
||||
],
|
||||
name: 'Sample exception item',
|
||||
type: 'simple',
|
||||
expire_time: expiredDate,
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body } = await postBulkAction()
|
||||
.send({
|
||||
query: '',
|
||||
action: BulkActionType.duplicate,
|
||||
duplicate: { include_exceptions: true, include_expired_exceptions: false },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: foundItems } = await supertest
|
||||
.get(
|
||||
`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${body.attributes.results.created[0].exceptions_list[1].list_id}`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
// Item should NOT have been duplicated, since it is expired
|
||||
expect(foundItems.total).to.eql(0);
|
||||
|
||||
expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 });
|
||||
|
||||
// Check that the duplicated rule is returned with the response
|
||||
expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`);
|
||||
|
||||
// Check that the exceptions are duplicted
|
||||
expect(body.attributes.results.created[0].exceptions_list).to.eql([
|
||||
{
|
||||
type: exceptionList.type,
|
||||
list_id: exceptionList.list_id,
|
||||
id: exceptionList.id,
|
||||
namespace_type: exceptionList.namespace_type,
|
||||
},
|
||||
{
|
||||
id: body.attributes.results.created[0].exceptions_list[1].id,
|
||||
list_id: body.attributes.results.created[0].exceptions_list[1].list_id,
|
||||
namespace_type: 'single',
|
||||
type: 'rule_default',
|
||||
},
|
||||
]);
|
||||
|
||||
// Check that the updates have been persisted
|
||||
const { body: rulesResponse } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}/_find`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
|
||||
expect(rulesResponse.total).to.eql(2);
|
||||
});
|
||||
|
||||
it('should duplicate rule with a legacy action', async () => {
|
||||
const ruleId = 'ruleId';
|
||||
const [connector, ruleToDuplicate] = await Promise.all([
|
||||
|
@ -462,7 +676,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
.send({
|
||||
query: '',
|
||||
action: BulkActionType.duplicate,
|
||||
duplicate: { include_exceptions: false },
|
||||
duplicate: { include_exceptions: false, include_expired_exceptions: false },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import {
|
||||
ENDPOINT_LIST_URL,
|
||||
EXCEPTION_LIST_ITEM_URL,
|
||||
EXCEPTION_LIST_URL,
|
||||
} from '@kbn/securitysolution-list-constants';
|
||||
import { getExceptionResponseMockWithoutAutoGeneratedValues } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
|
||||
import { getCreateExceptionListDetectionSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock';
|
||||
import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
|
||||
describe('duplicate_exception_lists', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllExceptions(supertest, log);
|
||||
});
|
||||
|
||||
it('should duplicate a list with no exception items', async () => {
|
||||
// create an exception list
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getCreateExceptionListDetectionSchemaMock())
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(
|
||||
`${EXCEPTION_LIST_URL}/_duplicate?list_id=${
|
||||
getCreateExceptionListDetectionSchemaMock().list_id
|
||||
}&namespace_type=single&include_expired_exceptions=true`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
|
||||
const bodyToCompare = removeExceptionListServerGeneratedProperties(body);
|
||||
expect(bodyToCompare).to.eql({
|
||||
...getExceptionResponseMockWithoutAutoGeneratedValues(),
|
||||
type: 'detection',
|
||||
list_id: body.list_id,
|
||||
name: `${getCreateExceptionListDetectionSchemaMock().name} [Duplicate]`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should duplicate a list and its items', async () => {
|
||||
// create an exception list
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getCreateExceptionListDetectionSchemaMock())
|
||||
.expect(200);
|
||||
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_ITEM_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
...getCreateExceptionListItemMinimalSchemaMock(),
|
||||
list_id: getCreateExceptionListDetectionSchemaMock().list_id,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: listBody } = await supertest
|
||||
.post(
|
||||
`${EXCEPTION_LIST_URL}/_duplicate?list_id=${
|
||||
getCreateExceptionListDetectionSchemaMock().list_id
|
||||
}&namespace_type=single&include_expired_exceptions=true`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${listBody.list_id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const listBodyToCompare = removeExceptionListServerGeneratedProperties(listBody);
|
||||
expect(listBodyToCompare).to.eql({
|
||||
...getExceptionResponseMockWithoutAutoGeneratedValues(),
|
||||
type: 'detection',
|
||||
list_id: listBody.list_id,
|
||||
name: `${getCreateExceptionListDetectionSchemaMock().name} [Duplicate]`,
|
||||
});
|
||||
|
||||
expect(body.total).to.eql(1);
|
||||
});
|
||||
|
||||
it('should duplicate a list with expired exception items', async () => {
|
||||
const expiredDate = new Date(Date.now() - 1000000).toISOString();
|
||||
|
||||
// create an exception list
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getCreateExceptionListDetectionSchemaMock())
|
||||
.expect(200);
|
||||
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_ITEM_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ ...getCreateExceptionListItemMinimalSchemaMock(), expire_time: expiredDate })
|
||||
.expect(200);
|
||||
|
||||
const { body: listBody } = await supertest
|
||||
.post(
|
||||
`${EXCEPTION_LIST_URL}/_duplicate?list_id=${
|
||||
getCreateExceptionListDetectionSchemaMock().list_id
|
||||
}&namespace_type=single&include_expired_exceptions=true`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${listBody.list_id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(body.total).to.eql(1);
|
||||
});
|
||||
|
||||
it('should duplicate a list and EXCLUDE expired exception items when "include_expired_exceptions" set to "false"', async () => {
|
||||
const expiredDate = new Date(Date.now() - 1000000).toISOString();
|
||||
|
||||
// create an exception list
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getCreateExceptionListDetectionSchemaMock())
|
||||
.expect(200);
|
||||
|
||||
await supertest
|
||||
.post(EXCEPTION_LIST_ITEM_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
...getCreateExceptionListItemMinimalSchemaMock(),
|
||||
list_id: getCreateExceptionListDetectionSchemaMock().list_id,
|
||||
expire_time: expiredDate,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: listBody } = await supertest
|
||||
.post(
|
||||
`${EXCEPTION_LIST_URL}/_duplicate?list_id=${
|
||||
getCreateExceptionListDetectionSchemaMock().list_id
|
||||
}&namespace_type=single&include_expired_exceptions=false`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${listBody.list_id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(body.total).to.eql(0);
|
||||
});
|
||||
|
||||
describe('error states', () => {
|
||||
it('should cause a 409 if list does not exist', async () => {
|
||||
const { body } = await supertest
|
||||
.post(
|
||||
`${EXCEPTION_LIST_URL}/_duplicate?list_id=exception_list_id&namespace_type=agnostic&include_expired_exceptions=true`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(404);
|
||||
|
||||
expect(body).to.eql({
|
||||
message: 'exception list id: "exception_list_id" does not exist',
|
||||
status_code: 404,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cause a 405 if trying to duplicate a reserved exception list type', async () => {
|
||||
// create an exception list
|
||||
await supertest.post(ENDPOINT_LIST_URL).set('kbn-xsrf', 'true').expect(200);
|
||||
|
||||
const { body } = await supertest
|
||||
.post(
|
||||
`${EXCEPTION_LIST_URL}/_duplicate?list_id=endpoint_list&namespace_type=agnostic&include_expired_exceptions=true`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(405);
|
||||
|
||||
expect(body).to.eql({
|
||||
message:
|
||||
'unable to duplicate exception list with list_id: endpoint_list - action not allowed',
|
||||
status_code: 405,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -19,6 +19,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./update_list_items'));
|
||||
loadTestFile(require.resolve('./delete_lists'));
|
||||
loadTestFile(require.resolve('./delete_list_items'));
|
||||
loadTestFile(require.resolve('./duplicate_exception_list'));
|
||||
loadTestFile(require.resolve('./find_lists'));
|
||||
loadTestFile(require.resolve('./find_list_items'));
|
||||
loadTestFile(require.resolve('./find_lists_by_size'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue