Skip to content

Commit

Permalink
[Security Solution][Lists] - Fix exception list with comments import …
Browse files Browse the repository at this point in the history
…bug (elastic#124909)

### Summary

Addresses elastic#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 f894d86)
  • Loading branch information
yctercero authored and kibanamachine committed Feb 16, 2022
1 parent 8f30e46 commit 070a669
Show file tree
Hide file tree
Showing 16 changed files with 666 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
Original file line number Diff line number Diff line change
@@ -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
);
Original file line number Diff line number Diff line change
Expand Up @@ -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({});
});
Expand All @@ -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({});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Original file line number Diff line number Diff line change
@@ -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({});
});
});
});
Original file line number Diff line number Diff line change
@@ -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>;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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();
Expand Down
Loading

0 comments on commit 070a669

Please sign in to comment.