mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
### Summary
Addresses https://github.com/elastic/kibana/issues/124742
#### Issue TLDR
Import of rules that reference exception items with comments fail. Failure message states that comments cannot include `created_at`, `created_by`, `id`.
(cherry picked from commit f894d8673b
)
Co-authored-by: Yara Tercero <yctercero@users.noreply.github.com>
This commit is contained in:
parent
fe9b14c5f8
commit
91ee460a0a
16 changed files with 666 additions and 17 deletions
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { ImportCommentsArray } from '../import_comment';
|
||||
import { DefaultImportCommentsArray } from '../default_import_comments_array';
|
||||
import { getCommentsArrayMock } from '../comment/index.mock';
|
||||
import { getCreateCommentsArrayMock } from '../create_comment/index.mock';
|
||||
|
||||
describe('default_import_comments_array', () => {
|
||||
test('it should pass validation when supplied an empty array', () => {
|
||||
const payload: ImportCommentsArray = [];
|
||||
const decoded = DefaultImportCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should pass validation when supplied an array of comments', () => {
|
||||
const payload: ImportCommentsArray = getCommentsArrayMock();
|
||||
const decoded = DefaultImportCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should pass validation when supplied an array of new comments', () => {
|
||||
const payload: ImportCommentsArray = getCreateCommentsArrayMock();
|
||||
const decoded = DefaultImportCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should pass validation when supplied an array of new and existing comments', () => {
|
||||
const payload: ImportCommentsArray = [
|
||||
...getCommentsArrayMock(),
|
||||
...getCreateCommentsArrayMock(),
|
||||
];
|
||||
const decoded = DefaultImportCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should fail validation when supplied an array of numbers', () => {
|
||||
const payload = [1];
|
||||
const decoded = DefaultImportCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "DefaultImportComments"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should fail validation when supplied an array of strings', () => {
|
||||
const payload = ['some string'];
|
||||
const decoded = DefaultImportCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "some string" supplied to "DefaultImportComments"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should return a default array entry', () => {
|
||||
const payload = null;
|
||||
const decoded = DefaultImportCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { Either } from 'fp-ts/lib/Either';
|
||||
import { importComment, ImportCommentsArray } from '../import_comment';
|
||||
|
||||
/**
|
||||
* Types the DefaultImportCommentsArray as:
|
||||
* - If null or undefined, then a default array of type ImportCommentsArray will be set
|
||||
*/
|
||||
export const DefaultImportCommentsArray = new t.Type<
|
||||
ImportCommentsArray,
|
||||
ImportCommentsArray,
|
||||
unknown
|
||||
>(
|
||||
'DefaultImportComments',
|
||||
t.array(importComment).is,
|
||||
(input, context): Either<t.Errors, ImportCommentsArray> =>
|
||||
input == null ? t.success([]) : t.array(importComment).validate(input, context),
|
||||
t.identity
|
||||
);
|
|
@ -38,7 +38,7 @@ describe('default_update_comments_array', () => {
|
|||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"',
|
||||
'Invalid value "1" supplied to "DefaultUpdateComments"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
@ -49,7 +49,7 @@ describe('default_update_comments_array', () => {
|
|||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"',
|
||||
'Invalid value "some string" supplied to "DefaultUpdateComments"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
|
|
@ -11,17 +11,17 @@ import { Either } from 'fp-ts/lib/Either';
|
|||
import { updateCommentsArray, UpdateCommentsArray } from '../update_comment';
|
||||
|
||||
/**
|
||||
* Types the DefaultCommentsUpdate as:
|
||||
* - If null or undefined, then a default array of type entry will be set
|
||||
* Types the DefaultUpdateComments as:
|
||||
* - If null or undefined, then a default array of type UpdateCommentsArray will be set
|
||||
*/
|
||||
export const DefaultUpdateCommentsArray = new t.Type<
|
||||
UpdateCommentsArray,
|
||||
UpdateCommentsArray,
|
||||
unknown
|
||||
>(
|
||||
'DefaultCreateComments',
|
||||
'DefaultUpdateComments',
|
||||
updateCommentsArray.is,
|
||||
(input): Either<t.Errors, UpdateCommentsArray> =>
|
||||
input == null ? t.success([]) : updateCommentsArray.decode(input),
|
||||
(input, context): Either<t.Errors, UpdateCommentsArray> =>
|
||||
input == null ? t.success([]) : updateCommentsArray.validate(input, context),
|
||||
t.identity
|
||||
);
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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 { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { getCommentsArrayMock, getCommentsMock } from '../comment/index.mock';
|
||||
import { getCreateCommentsArrayMock } from '../create_comment/index.mock';
|
||||
import {
|
||||
importComment,
|
||||
ImportCommentsArray,
|
||||
importCommentsArray,
|
||||
ImportCommentsArrayOrUndefined,
|
||||
importCommentsArrayOrUndefined,
|
||||
} from '.';
|
||||
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
|
||||
describe('ImportComment', () => {
|
||||
describe('importComment', () => {
|
||||
test('it passes validation with a typical comment', () => {
|
||||
const payload = getCommentsMock();
|
||||
const decoded = importComment.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it passes validation with a new comment', () => {
|
||||
const payload = { comment: 'new comment' };
|
||||
const decoded = importComment.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it fails validation when undefined', () => {
|
||||
const payload = undefined;
|
||||
const decoded = importComment.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "undefined" supplied to "(({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: NonEmptyString |})"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('importCommentsArray', () => {
|
||||
test('it passes validation an array of Comment', () => {
|
||||
const payload = getCommentsArrayMock();
|
||||
const decoded = importCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it passes validation an array of CreateComment', () => {
|
||||
const payload = getCreateCommentsArrayMock();
|
||||
const decoded = importCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it passes validation an array of Comment and CreateComment', () => {
|
||||
const payload = [...getCommentsArrayMock(), ...getCreateCommentsArrayMock()];
|
||||
const decoded = importCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it fails validation when undefined', () => {
|
||||
const payload = undefined;
|
||||
const decoded = importCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "undefined" supplied to "Array<(({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: NonEmptyString |})>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it fails validation when array includes non ImportComment types', () => {
|
||||
const payload = [1] as unknown as ImportCommentsArray;
|
||||
const decoded = importCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<(({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: NonEmptyString |})>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('importCommentsArrayOrUndefined', () => {
|
||||
test('it passes validation an array of ImportComment', () => {
|
||||
const payload = [...getCommentsArrayMock(), ...getCreateCommentsArrayMock()];
|
||||
const decoded = importCommentsArrayOrUndefined.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it passes validation when undefined', () => {
|
||||
const payload = undefined;
|
||||
const decoded = importCommentsArrayOrUndefined.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it fails validation when array includes non ImportComment types', () => {
|
||||
const payload = [1] as unknown as ImportCommentsArrayOrUndefined;
|
||||
const decoded = importCommentsArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "1" supplied to "Array<(({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: NonEmptyString |})>"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { createComment } from '../create_comment';
|
||||
import { comment } from '../comment';
|
||||
|
||||
export const importComment = t.union([comment, createComment]);
|
||||
|
||||
export type ImportComment = t.TypeOf<typeof importComment>;
|
||||
export const importCommentsArray = t.array(importComment);
|
||||
export type ImportCommentsArray = t.TypeOf<typeof importCommentsArray>;
|
||||
export const importCommentsArrayOrUndefined = t.union([importCommentsArray, t.undefined]);
|
||||
export type ImportCommentsArrayOrUndefined = t.TypeOf<typeof importCommentsArrayOrUndefined>;
|
|
@ -13,6 +13,7 @@ export * from './created_by';
|
|||
export * from './cursor';
|
||||
export * from './default_namespace';
|
||||
export * from './default_namespace_array';
|
||||
export * from './default_import_comments_array';
|
||||
export * from './description';
|
||||
export * from './deserializer';
|
||||
export * from './endpoint';
|
||||
|
@ -29,6 +30,7 @@ export * from './exception_list_item_type';
|
|||
export * from './filter';
|
||||
export * from './id';
|
||||
export * from './immutable';
|
||||
export * from './import_comment';
|
||||
export * from './item_id';
|
||||
export * from './list_id';
|
||||
export * from './list_operator';
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from '.';
|
||||
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
|
||||
describe('CommentsUpdate', () => {
|
||||
describe('UpdateComment', () => {
|
||||
describe('updateComment', () => {
|
||||
test('it should pass validation when supplied typical comment update', () => {
|
||||
const payload = getUpdateCommentMock();
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
getImportExceptionsListItemSchemaDecodedMock,
|
||||
getImportExceptionsListItemSchemaMock,
|
||||
} from './index.mock';
|
||||
import { getCommentsArrayMock } from '../../common/comment/index.mock';
|
||||
|
||||
describe('import_list_item_schema', () => {
|
||||
test('it should validate a typical item request', () => {
|
||||
|
@ -27,6 +28,35 @@ describe('import_list_item_schema', () => {
|
|||
expect(message.schema).toEqual(getImportExceptionsListItemSchemaDecodedMock());
|
||||
});
|
||||
|
||||
test('it should validate a typical item request with comments', () => {
|
||||
const payload = {
|
||||
...getImportExceptionsListItemSchemaMock(),
|
||||
comments: getCommentsArrayMock(),
|
||||
};
|
||||
const decoded = importExceptionListItemSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual({
|
||||
...getImportExceptionsListItemSchemaDecodedMock(),
|
||||
comments: [
|
||||
{
|
||||
comment: 'some old comment',
|
||||
created_at: '2020-04-20T15:25:31.830Z',
|
||||
created_by: 'some user',
|
||||
id: 'uuid_here',
|
||||
},
|
||||
{
|
||||
comment: 'some old comment',
|
||||
created_at: '2020-04-20T15:25:31.830Z',
|
||||
created_by: 'some user',
|
||||
id: 'uuid_here',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('it should NOT accept an undefined for "item_id"', () => {
|
||||
const payload: Partial<ReturnType<typeof getImportExceptionsListItemSchemaMock>> =
|
||||
getImportExceptionsListItemSchemaMock();
|
||||
|
|
|
@ -29,8 +29,8 @@ import { nonEmptyEntriesArray } from '../../common/non_empty_entries_array';
|
|||
import { exceptionListItemType } from '../../common/exception_list_item_type';
|
||||
import { ItemId } from '../../common/item_id';
|
||||
import { EntriesArray } from '../../common/entries';
|
||||
import { CreateCommentsArray } from '../../common/create_comment';
|
||||
import { DefaultCreateCommentsArray } from '../../common/default_create_comments_array';
|
||||
import { DefaultImportCommentsArray } from '../../common/default_import_comments_array';
|
||||
import { ImportCommentsArray } from '../../common';
|
||||
|
||||
/**
|
||||
* Differences from this and the createExceptionsListItemSchema are
|
||||
|
@ -56,7 +56,7 @@ export const importExceptionListItemSchema = t.intersection([
|
|||
t.exact(
|
||||
t.partial({
|
||||
id, // defaults to undefined if not set during decode
|
||||
comments: DefaultCreateCommentsArray, // defaults to empty array if not set during decode
|
||||
comments: DefaultImportCommentsArray, // defaults to empty array if not set during decode
|
||||
created_at, // defaults undefined if not set during decode
|
||||
updated_at, // defaults undefined if not set during decode
|
||||
created_by, // defaults undefined if not set during decode
|
||||
|
@ -78,7 +78,7 @@ export type ImportExceptionListItemSchemaDecoded = Omit<
|
|||
ImportExceptionListItemSchema,
|
||||
'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments'
|
||||
> & {
|
||||
comments: CreateCommentsArray;
|
||||
comments: ImportCommentsArray;
|
||||
tags: Tags;
|
||||
item_id: ItemId;
|
||||
entries: EntriesArray;
|
||||
|
|
|
@ -24,6 +24,7 @@ import { PromiseStream } from '../../import_exception_list_and_items';
|
|||
import {
|
||||
createExceptionsStreamFromNdjson,
|
||||
exceptionsChecksFromArray,
|
||||
manageExceptionComments,
|
||||
} from './create_exceptions_stream_logic';
|
||||
|
||||
describe('create_exceptions_stream_logic', () => {
|
||||
|
@ -338,4 +339,49 @@ describe('create_exceptions_stream_logic', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('manageExceptionComments', () => {
|
||||
test('returns empty array if passed in "comments" undefined', () => {
|
||||
const result = manageExceptionComments(undefined);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns empty array if passed in "comments" empty array', () => {
|
||||
const result = manageExceptionComments([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns formatted existing comment', () => {
|
||||
const result = manageExceptionComments([
|
||||
{
|
||||
comment: 'some old comment',
|
||||
created_at: '2020-04-20T15:25:31.830Z',
|
||||
created_by: 'kibana',
|
||||
id: 'uuid_here',
|
||||
updated_at: '2020-05-20T15:25:31.830Z',
|
||||
updated_by: 'lily',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
comment: 'some old comment',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('returns formatted new comment', () => {
|
||||
const result = manageExceptionComments([
|
||||
{
|
||||
comment: 'some new comment',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
comment: 'some new comment',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,9 @@ import { has } from 'lodash/fp';
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import {
|
||||
CreateCommentsArray,
|
||||
ExportExceptionDetails,
|
||||
ImportCommentsArray,
|
||||
ImportExceptionListItemSchema,
|
||||
ImportExceptionListItemSchemaDecoded,
|
||||
ImportExceptionListSchemaDecoded,
|
||||
|
@ -120,6 +122,24 @@ export const sortExceptionsStream = (): Transform => {
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates any comments associated with exception items to resemble
|
||||
* comment creation schema.
|
||||
* See issue for context https://github.com/elastic/kibana/issues/124742#issuecomment-1033082093
|
||||
* @returns {array} comments reformatted properly for import
|
||||
*/
|
||||
export const manageExceptionComments = (
|
||||
comments: ImportCommentsArray | undefined
|
||||
): CreateCommentsArray => {
|
||||
if (comments == null || !comments.length) {
|
||||
return [];
|
||||
} else {
|
||||
return comments.map(({ comment }) => ({
|
||||
comment,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Validating exceptions logic
|
||||
|
@ -206,8 +226,9 @@ export const validateExceptionsItems = (
|
|||
|
||||
return items.map((item: ImportExceptionListItemSchema | Error) => {
|
||||
if (!(item instanceof Error)) {
|
||||
const decodedItem = importExceptionListItemSchema.decode(item);
|
||||
const checkedItem = exactCheck(item, decodedItem);
|
||||
const itemWithUpdatedComments = { ...item, comments: manageExceptionComments(item.comments) };
|
||||
const decodedItem = importExceptionListItemSchema.decode(itemWithUpdatedComments);
|
||||
const checkedItem = exactCheck(itemWithUpdatedComments, decodedItem);
|
||||
|
||||
return pipe(checkedItem, fold(onLeft, onRight));
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
|
||||
|
||||
import { deleteAllExceptions } from '../../../lists_api_integration/utils';
|
||||
import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock';
|
||||
import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
binaryToString,
|
||||
createRule,
|
||||
createSignalsIndex,
|
||||
deleteAllAlerts,
|
||||
deleteSignalsIndex,
|
||||
getSimpleRule,
|
||||
} from '../../utils';
|
||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
||||
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||
|
||||
// This test was meant to be more full flow, ensuring that
|
||||
// exported rules are able to be reimported as opposed to
|
||||
// testing the import/export functionality separately
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const log = getService('log');
|
||||
|
||||
describe('import_export_rules_flow', () => {
|
||||
beforeEach(async () => {
|
||||
await createSignalsIndex(supertest, log);
|
||||
await createUserAndRole(getService, ROLES.soc_manager);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteUserAndRole(getService, ROLES.soc_manager);
|
||||
await deleteAllExceptions(supertest, log);
|
||||
await deleteSignalsIndex(supertest, log);
|
||||
await deleteAllAlerts(supertest, log);
|
||||
});
|
||||
|
||||
it('should be able to reimport a rule referencing an exception list with existing comments', async () => {
|
||||
// create an exception list
|
||||
const { body: exceptionBody } = await supertestWithoutAuth
|
||||
.post(EXCEPTION_LIST_URL)
|
||||
.auth(ROLES.soc_manager, 'changeme')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(getCreateExceptionListMinimalSchemaMock())
|
||||
.expect(200);
|
||||
|
||||
// create an exception list item
|
||||
const { body: exceptionItemBody } = await supertestWithoutAuth
|
||||
.post(EXCEPTION_LIST_ITEM_URL)
|
||||
.auth(ROLES.soc_manager, 'changeme')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
...getCreateExceptionListItemMinimalSchemaMock(),
|
||||
comments: [{ comment: 'this exception item rocks' }],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: exceptionItem } = await supertest
|
||||
.get(`${EXCEPTION_LIST_ITEM_URL}?item_id=${exceptionItemBody.item_id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
|
||||
expect(exceptionItem.comments).to.eql([
|
||||
{
|
||||
comment: 'this exception item rocks',
|
||||
created_at: `${exceptionItem.comments[0].created_at}`,
|
||||
created_by: 'soc_manager',
|
||||
id: `${exceptionItem.comments[0].id}`,
|
||||
},
|
||||
]);
|
||||
|
||||
await createRule(supertest, log, {
|
||||
...getSimpleRule(),
|
||||
exceptions_list: [
|
||||
{
|
||||
id: exceptionBody.id,
|
||||
list_id: exceptionBody.list_id,
|
||||
type: exceptionBody.type,
|
||||
namespace_type: exceptionBody.namespace_type,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_export`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200)
|
||||
.parse(binaryToString);
|
||||
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true&overwrite_exceptions=true`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach('file', Buffer.from(body), 'rules.ndjson')
|
||||
.expect(200);
|
||||
|
||||
const { body: exceptionItemFind2 } = await supertest
|
||||
.get(`${EXCEPTION_LIST_ITEM_URL}?item_id=${exceptionItemBody.item_id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
|
||||
// NOTE: Existing comment is uploaded successfully
|
||||
// however, the meta now reflects who imported it,
|
||||
// not who created the initial comment
|
||||
expect(exceptionItemFind2.comments).to.eql([
|
||||
{
|
||||
comment: 'this exception item rocks',
|
||||
created_at: `${exceptionItemFind2.comments[0].created_at}`,
|
||||
created_by: 'elastic',
|
||||
id: `${exceptionItemFind2.comments[0].id}`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
|
||||
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
|
||||
import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock';
|
||||
import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
@ -831,7 +831,33 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
type: 'detection',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
getImportExceptionsListItemSchemaMock('test_item_id', 'i_exist'),
|
||||
{
|
||||
description: 'some description',
|
||||
entries: [
|
||||
{
|
||||
entries: [
|
||||
{
|
||||
field: 'nested.field',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'some value',
|
||||
},
|
||||
],
|
||||
field: 'some.parentField',
|
||||
type: 'nested',
|
||||
},
|
||||
{
|
||||
field: 'some.not.nested.field',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'some value',
|
||||
},
|
||||
],
|
||||
item_id: 'item_id_1',
|
||||
list_id: 'i_exist',
|
||||
name: 'Query with a rule id',
|
||||
type: 'simple',
|
||||
},
|
||||
])
|
||||
),
|
||||
'rules.ndjson'
|
||||
|
@ -871,6 +897,135 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
exceptions_success_count: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve exception references that include comments', async () => {
|
||||
// So importing a rule that references an exception list
|
||||
// Keep in mind, no exception lists or rules exist yet
|
||||
const simpleRule: ReturnType<typeof getSimpleRule> = {
|
||||
...getSimpleRule('rule-1'),
|
||||
exceptions_list: [
|
||||
{
|
||||
id: 'abc',
|
||||
list_id: 'i_exist',
|
||||
type: 'detection',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Importing the "simpleRule", along with the exception list
|
||||
// it's referencing and the list's item
|
||||
const { body } = await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach(
|
||||
'file',
|
||||
Buffer.from(
|
||||
toNdJsonString([
|
||||
simpleRule,
|
||||
{
|
||||
...getImportExceptionsListSchemaMock('i_exist'),
|
||||
id: 'abc',
|
||||
type: 'detection',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
{
|
||||
comments: [
|
||||
{
|
||||
comment: 'This is an exception to the rule',
|
||||
created_at: '2022-02-04T02:27:40.938Z',
|
||||
created_by: 'elastic',
|
||||
id: '845fc456-91ff-4530-bcc1-5b7ebd2f75b5',
|
||||
},
|
||||
{
|
||||
comment: 'I decided to add a new comment',
|
||||
},
|
||||
],
|
||||
description: 'some description',
|
||||
entries: [
|
||||
{
|
||||
entries: [
|
||||
{
|
||||
field: 'nested.field',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'some value',
|
||||
},
|
||||
],
|
||||
field: 'some.parentField',
|
||||
type: 'nested',
|
||||
},
|
||||
{
|
||||
field: 'some.not.nested.field',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'some value',
|
||||
},
|
||||
],
|
||||
item_id: 'item_id_1',
|
||||
list_id: 'i_exist',
|
||||
name: 'Query with a rule id',
|
||||
type: 'simple',
|
||||
},
|
||||
])
|
||||
),
|
||||
'rules.ndjson'
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
const { body: ruleResponse } = await supertest
|
||||
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`)
|
||||
.send()
|
||||
.expect(200);
|
||||
const bodyToCompare = removeServerGeneratedProperties(ruleResponse);
|
||||
const referencedExceptionList = ruleResponse.exceptions_list[0];
|
||||
|
||||
// create an exception list
|
||||
const { body: exceptionBody } = await supertest
|
||||
.get(
|
||||
`${EXCEPTION_LIST_URL}?list_id=${referencedExceptionList.list_id}&id=${referencedExceptionList.id}`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
|
||||
expect(bodyToCompare.exceptions_list).to.eql([
|
||||
{
|
||||
id: exceptionBody.id,
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
]);
|
||||
|
||||
const { body: exceptionItemBody } = await supertest
|
||||
.get(`${EXCEPTION_LIST_ITEM_URL}?item_id="item_id_1"`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
|
||||
expect(exceptionItemBody.comments).to.eql([
|
||||
{
|
||||
comment: 'This is an exception to the rule',
|
||||
created_at: `${exceptionItemBody.comments[0].created_at}`,
|
||||
created_by: 'elastic',
|
||||
id: `${exceptionItemBody.comments[0].id}`,
|
||||
},
|
||||
{
|
||||
comment: 'I decided to add a new comment',
|
||||
created_at: `${exceptionItemBody.comments[1].created_at}`,
|
||||
created_by: 'elastic',
|
||||
id: `${exceptionItemBody.comments[1].id}`,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(body).to.eql({
|
||||
success: true,
|
||||
success_count: 1,
|
||||
errors: [],
|
||||
exceptions_errors: [],
|
||||
exceptions_success: true,
|
||||
exceptions_success_count: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,6 +33,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./generating_signals'));
|
||||
loadTestFile(require.resolve('./get_prepackaged_rules_status'));
|
||||
loadTestFile(require.resolve('./import_rules'));
|
||||
loadTestFile(require.resolve('./import_export_rules'));
|
||||
loadTestFile(require.resolve('./read_rules'));
|
||||
loadTestFile(require.resolve('./resolve_read_rules'));
|
||||
loadTestFile(require.resolve('./update_rules'));
|
||||
|
|
|
@ -1049,8 +1049,8 @@ export const countDownTest = async (
|
|||
* and error about the race condition.
|
||||
* rule a second attempt. It only re-tries adding the rule if it encounters a conflict once.
|
||||
* @param supertest The supertest deps
|
||||
* @param rule The rule to create
|
||||
* @param log The tooling logger
|
||||
* @param rule The rule to create
|
||||
*/
|
||||
export const createRule = async (
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue