[Security Solution][Lists] - Fix exception list with comments import bug (#124909)

### 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`.
This commit is contained in:
Yara Tercero 2022-02-16 07:20:17 -08:00 committed by GitHub
parent 3fe08aff12
commit f894d8673b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 666 additions and 17 deletions

View file

@ -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([]);
});
});

View file

@ -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
);

View file

@ -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({});
});

View file

@ -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
);

View file

@ -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({});
});
});
});

View file

@ -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>;

View file

@ -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';

View file

@ -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();

View file

@ -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();

View file

@ -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;

View file

@ -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',
},
]);
});
});
});

View file

@ -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 {

View file

@ -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}`,
},
]);
});
});
};

View file

@ -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,
});
});
});
});
});

View file

@ -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'));

View file

@ -1043,8 +1043,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>,