diff --git a/x-pack/plugins/lists/README.md b/x-pack/plugins/lists/README.md index cb343c95b0103..cdd7813792fc3 100644 --- a/x-pack/plugins/lists/README.md +++ b/x-pack/plugins/lists/README.md @@ -149,7 +149,7 @@ And you can attach exception list items like so: "malware", "os:linux" ], - "comment": [], + "comments": [], "created_at": "2020-05-28T19:17:21.099Z", "created_by": "yo", "description": "This is a sample endpoint type exception", diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index d8e4dfba1599e..0df0aeff593db 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -31,6 +31,7 @@ export const VALUE_2 = '255.255.255'; export const NAMESPACE_TYPE = 'single'; // Exception List specific +export const ID = 'uuid_here'; export const ENDPOINT_TYPE = 'endpoint'; export const ENTRIES = [ { field: 'some.field', match: 'some value', match_any: undefined, operator: 'included' }, @@ -38,4 +39,4 @@ export const ENTRIES = [ export const ITEM_TYPE = 'simple'; export const _TAGS = []; export const TAGS = []; -export const COMMENT = []; +export const COMMENTS = []; diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 7f086647bbc75..3d1f537a9ca41 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -86,12 +86,6 @@ export type ExceptionListItemType = t.TypeOf; export const list_type = t.keyof({ item: null, list: null }); export type ListType = t.TypeOf; -// TODO: Investigate what the deep structure of a comment is really going to be and then change this to use that deep structure with a default array -export const comment = DefaultStringArray; -export type Comment = t.TypeOf; -export const commentOrUndefined = t.union([comment, t.undefined]); -export type CommentOrUndefined = t.TypeOf; - export const item_id = NonEmptyString; export type ItemId = t.TypeOf; export const itemIdOrUndefined = t.union([item_id, t.undefined]); diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts index f9af10245b7ee..0450849931b30 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts @@ -5,7 +5,7 @@ */ import { - COMMENT, + COMMENTS, DESCRIPTION, ENTRIES, ITEM_TYPE, @@ -21,7 +21,7 @@ import { CreateExceptionListItemSchema } from './create_exception_list_item_sche export const getCreateExceptionListItemSchemaMock = (): CreateExceptionListItemSchema => ({ _tags: _TAGS, - comment: COMMENT, + comments: COMMENTS, description: DESCRIPTION, entries: ENTRIES, item_id: undefined, diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts new file mode 100644 index 0000000000000..61437b1f04ce3 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { + CreateExceptionListItemSchema, + createExceptionListItemSchema, +} from './create_exception_list_item_schema'; +import { getCreateExceptionListItemSchemaMock } from './create_exception_list_item_schema.mock'; + +describe('create_exception_list_schema', () => { + test('it should validate a typical exception list item request', () => { + const payload = getCreateExceptionListItemSchemaMock(); + const outputPayload = getCreateExceptionListItemSchemaMock(); + const decoded = createExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should not accept an undefined for "description"', () => { + const payload = getCreateExceptionListItemSchemaMock(); + delete payload.description; + const decoded = createExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "description"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not accept an undefined for "name"', () => { + const payload = getCreateExceptionListItemSchemaMock(); + delete payload.name; + const decoded = createExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "name"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not accept an undefined for "type"', () => { + const payload = getCreateExceptionListItemSchemaMock(); + delete payload.type; + const decoded = createExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not accept an undefined for "list_id"', () => { + const inputPayload = getCreateExceptionListItemSchemaMock(); + delete inputPayload.list_id; + const decoded = createExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, 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 accept an undefined for "meta" but strip it out', () => { + const payload = getCreateExceptionListItemSchemaMock(); + const outputPayload = getCreateExceptionListItemSchemaMock(); + delete payload.meta; + const decoded = createExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + delete outputPayload.meta; + outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "comments" but return an array', () => { + const inputPayload = getCreateExceptionListItemSchemaMock(); + const outputPayload = getCreateExceptionListItemSchemaMock(); + delete inputPayload.comments; + outputPayload.comments = []; + const decoded = createExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "entries" but return an array', () => { + const inputPayload = getCreateExceptionListItemSchemaMock(); + const outputPayload = getCreateExceptionListItemSchemaMock(); + delete inputPayload.entries; + outputPayload.entries = []; + const decoded = createExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "namespace_type" but return enum "single"', () => { + const inputPayload = getCreateExceptionListItemSchemaMock(); + const outputPayload = getCreateExceptionListItemSchemaMock(); + delete inputPayload.namespace_type; + outputPayload.namespace_type = 'single'; + const decoded = createExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "tags" but return an array', () => { + const inputPayload = getCreateExceptionListItemSchemaMock(); + const outputPayload = getCreateExceptionListItemSchemaMock(); + delete inputPayload.tags; + outputPayload.tags = []; + const decoded = createExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "_tags" but return an array', () => { + const inputPayload = getCreateExceptionListItemSchemaMock(); + const outputPayload = getCreateExceptionListItemSchemaMock(); + delete inputPayload._tags; + outputPayload._tags = []; + const decoded = createExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + outputPayload.item_id = (message.schema as CreateExceptionListItemSchema).item_id; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "item_id" and auto generate a uuid', () => { + const inputPayload = getCreateExceptionListItemSchemaMock(); + delete inputPayload.item_id; + const decoded = createExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as CreateExceptionListItemSchema).item_id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); + + test('it should accept an undefined for "item_id" and generate a correct body not counting the uuid', () => { + const inputPayload = getCreateExceptionListItemSchemaMock(); + delete inputPayload.item_id; + const decoded = createExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + delete (message.schema as CreateExceptionListItemSchema).item_id; + expect(message.schema).toEqual(inputPayload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: CreateExceptionListItemSchema & { + extraKey?: string; + } = getCreateExceptionListItemSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = createExceptionListItemSchema.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({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index 4322ff4c2801b..f593b5d164035 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -14,7 +14,6 @@ import { Tags, _Tags, _tags, - comment, description, exceptionListItemType, list_id, @@ -24,7 +23,7 @@ import { tags, } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; -import { DefaultEntryArray } from '../types'; +import { CommentsPartialArray, DefaultCommentsPartialArray, DefaultEntryArray } from '../types'; import { EntriesArray } from '../types/entries'; import { DefaultUuid } from '../../siem_common_deps'; @@ -40,7 +39,7 @@ export const createExceptionListItemSchema = t.intersection([ t.exact( t.partial({ _tags, // defaults to empty array if not set during decode - comment, // defaults to empty array if not set during decode + comments: DefaultCommentsPartialArray, // defaults to empty array if not set during decode entries: DefaultEntryArray, // defaults to empty array if not set during decode item_id: DefaultUuid, // defaults to GUID (uuid v4) if not set during decode meta, // defaults to undefined if not set during decode @@ -61,9 +60,10 @@ export type CreateExceptionListItemSchema = RequiredKeepUndefined< export type CreateExceptionListItemSchemaDecoded = Identity< Omit< CreateExceptionListItemSchema, - '_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type' + '_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments' > & { _tags: _Tags; + comments: CommentsPartialArray; tags: Tags; item_id: ItemId; entries: EntriesArray; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..e8936f0bdc6d4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + COMMENTS, + DESCRIPTION, + ENTRIES, + ID, + ITEM_TYPE, + LIST_ITEM_ID, + META, + NAME, + NAMESPACE_TYPE, + TAGS, + _TAGS, +} from '../../constants.mock'; + +import { UpdateExceptionListItemSchema } from './update_exception_list_item_schema'; + +export const getUpdateExceptionListItemSchemaMock = (): UpdateExceptionListItemSchema => ({ + _tags: _TAGS, + comments: COMMENTS, + description: DESCRIPTION, + entries: ENTRIES, + id: ID, + item_id: LIST_ITEM_ID, + meta: META, + name: NAME, + namespace_type: NAMESPACE_TYPE, + tags: TAGS, + type: ITEM_TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts new file mode 100644 index 0000000000000..38541e205598b --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.test.ts @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { + UpdateExceptionListItemSchema, + updateExceptionListItemSchema, +} from './update_exception_list_item_schema'; +import { getUpdateExceptionListItemSchemaMock } from './update_exception_list_item_schema.mock'; + +describe('update_exception_list_item_schema', () => { + test('it should validate a typical exception list item request', () => { + const payload = getUpdateExceptionListItemSchemaMock(); + const decoded = updateExceptionListItemSchema.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 not accept an undefined for "description"', () => { + const payload = getUpdateExceptionListItemSchemaMock(); + delete payload.description; + const decoded = updateExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "description"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not accept an undefined for "name"', () => { + const payload = getUpdateExceptionListItemSchemaMock(); + delete payload.name; + const decoded = updateExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "name"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not accept an undefined for "type"', () => { + const payload = getUpdateExceptionListItemSchemaMock(); + delete payload.type; + const decoded = updateExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not accept a value for "list_id"', () => { + const payload: UpdateExceptionListItemSchema & { + list_id?: string; + } = getUpdateExceptionListItemSchemaMock(); + payload.list_id = 'some new list_id'; + const decoded = updateExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "list_id"']); + expect(message.schema).toEqual({}); + }); + + test('it should accept an undefined for "meta" but strip it out', () => { + const payload = getUpdateExceptionListItemSchemaMock(); + const outputPayload = getUpdateExceptionListItemSchemaMock(); + delete payload.meta; + const decoded = updateExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + delete outputPayload.meta; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "comments" but return an array', () => { + const inputPayload = getUpdateExceptionListItemSchemaMock(); + const outputPayload = getUpdateExceptionListItemSchemaMock(); + delete inputPayload.comments; + outputPayload.comments = []; + const decoded = updateExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "entries" but return an array', () => { + const inputPayload = getUpdateExceptionListItemSchemaMock(); + const outputPayload = getUpdateExceptionListItemSchemaMock(); + delete inputPayload.entries; + outputPayload.entries = []; + const decoded = updateExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "namespace_type" but return enum "single"', () => { + const inputPayload = getUpdateExceptionListItemSchemaMock(); + const outputPayload = getUpdateExceptionListItemSchemaMock(); + delete inputPayload.namespace_type; + outputPayload.namespace_type = 'single'; + const decoded = updateExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "tags" but return an array', () => { + const inputPayload = getUpdateExceptionListItemSchemaMock(); + const outputPayload = getUpdateExceptionListItemSchemaMock(); + delete inputPayload.tags; + outputPayload.tags = []; + const decoded = updateExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + test('it should accept an undefined for "_tags" but return an array', () => { + const inputPayload = getUpdateExceptionListItemSchemaMock(); + const outputPayload = getUpdateExceptionListItemSchemaMock(); + delete inputPayload._tags; + outputPayload._tags = []; + const decoded = updateExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(outputPayload); + }); + + // TODO: Is it expected behavior for it not to auto-generate a uui or throw + // error if item_id is not passed in? + xtest('it should accept an undefined for "item_id" and auto generate a uuid', () => { + const inputPayload = getUpdateExceptionListItemSchemaMock(); + delete inputPayload.item_id; + const decoded = updateExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect((message.schema as UpdateExceptionListItemSchema).item_id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); + + test('it should accept an undefined for "item_id" and generate a correct body not counting the uuid', () => { + const inputPayload = getUpdateExceptionListItemSchemaMock(); + delete inputPayload.item_id; + const decoded = updateExceptionListItemSchema.decode(inputPayload); + const checked = exactCheck(inputPayload, decoded); + const message = pipe(checked, foldLeftRight); + delete (message.schema as UpdateExceptionListItemSchema).item_id; + expect(message.schema).toEqual(inputPayload); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: UpdateExceptionListItemSchema & { + extraKey?: string; + } = getUpdateExceptionListItemSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = updateExceptionListItemSchema.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({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts index 3d66dad959c25..c32b15fecb571 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts @@ -13,7 +13,6 @@ import { Tags, _Tags, _tags, - comment, description, exceptionListItemType, id, @@ -23,8 +22,12 @@ import { tags, } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; -import { DefaultEntryArray } from '../types'; -import { EntriesArray } from '../types/entries'; +import { + CommentsPartialArray, + DefaultCommentsPartialArray, + DefaultEntryArray, + EntriesArray, +} from '../types'; export const updateExceptionListItemSchema = t.intersection([ t.exact( @@ -37,7 +40,7 @@ export const updateExceptionListItemSchema = t.intersection([ t.exact( t.partial({ _tags, // defaults to empty array if not set during decode - comment, // defaults to empty array if not set during decode + comments: DefaultCommentsPartialArray, // defaults to empty array if not set during decode entries: DefaultEntryArray, // defaults to empty array if not set during decode id, // defaults to undefined if not set during decode item_id: t.union([t.string, t.undefined]), @@ -57,8 +60,12 @@ export type UpdateExceptionListItemSchema = RequiredKeepUndefined< // This type is used after a decode since some things are defaults after a decode. export type UpdateExceptionListItemSchemaDecoded = Identity< - Omit & { + Omit< + UpdateExceptionListItemSchema, + '_tags' | 'tags' | 'entries' | 'namespace_type' | 'comments' + > & { _tags: _Tags; + comments: CommentsPartialArray; tags: Tags; entries: EntriesArray; namespace_type: NamespaceType; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts index 901715b601b80..663bfc7038330 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -8,7 +8,7 @@ import { ExceptionListItemSchema } from './exception_list_item_schema'; export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ _tags: ['endpoint', 'process', 'malware', 'os:linux'], - comment: [], + comments: [], created_at: '2020-04-23T00:19:13.289Z', created_by: 'user_name', description: 'This is a sample endpoint type exception', diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts new file mode 100644 index 0000000000000..b9d142fbccbee --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.test.ts @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; +import { ExceptionListItemSchema, exceptionListItemSchema } from './exception_list_item_schema'; + +describe('exception_list_item_schema', () => { + test('it should validate a typical exception list item response', () => { + const payload = getExceptionListItemSchemaMock(); + const decoded = exceptionListItemSchema.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 NOT accept an undefined for "id"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.id; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "undefined" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "list_id"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.list_id; + const decoded = exceptionListItemSchema.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 accept an undefined for "item_id"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.item_id; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "item_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "comments"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.comments; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "comments"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "entries"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.entries; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "entries"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "name"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.name; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "name"', + ]); + expect(message.schema).toEqual({}); + }); + + // TODO: Should this throw an error? "namespace_type" gets auto-populated + // with default "single", is that desired behavior? + xtest('it should NOT accept an undefined for "namespace_type"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.namespace_type; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "description"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.description; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "description"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should accept an undefined for "meta"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.meta; + const decoded = exceptionListItemSchema.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 NOT accept an undefined for "created_at"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.created_at; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "created_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "created_by"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.created_by; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "created_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "tie_breaker_id"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.tie_breaker_id; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "tie_breaker_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "type"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.type; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "updated_at"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.updated_at; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "updated_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT accept an undefined for "updated_by"', () => { + const payload = getExceptionListItemSchemaMock(); + delete payload.updated_by; + const decoded = exceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "updated_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: ExceptionListItemSchema & { + extraKey?: string; + } = getExceptionListItemSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = exceptionListItemSchema.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({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts index ab405c21d9c77..0de8fd72900af 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts @@ -10,7 +10,6 @@ import * as t from 'io-ts'; import { _tags, - commentOrUndefined, created_at, created_by, description, @@ -26,13 +25,13 @@ import { updated_at, updated_by, } from '../common/schemas'; -import { entriesArray } from '../types'; +import { commentsArray, entriesArray } from '../types'; // TODO: Should we use a partial here to reflect that this can JSON serialize meta, comment as non existent? export const exceptionListItemSchema = t.exact( t.type({ _tags, - comment: commentOrUndefined, + comments: commentsArray, created_at, created_by, description, diff --git a/x-pack/plugins/lists/common/schemas/saved_objects/exceptions_list_so_schema.ts b/x-pack/plugins/lists/common/schemas/saved_objects/exceptions_list_so_schema.ts index a08bab65c881c..0b61f122463f3 100644 --- a/x-pack/plugins/lists/common/schemas/saved_objects/exceptions_list_so_schema.ts +++ b/x-pack/plugins/lists/common/schemas/saved_objects/exceptions_list_so_schema.ts @@ -8,10 +8,9 @@ import * as t from 'io-ts'; -import { entriesArrayOrUndefined } from '../types'; +import { commentsArrayOrUndefined, entriesArrayOrUndefined } from '../types'; import { _tags, - commentOrUndefined, created_at, created_by, description, @@ -30,7 +29,7 @@ import { export const exceptionListSoSchema = t.exact( t.type({ _tags, - comment: commentOrUndefined, + comments: commentsArrayOrUndefined, created_at, created_by, description, diff --git a/x-pack/plugins/lists/common/schemas/types/comments.ts b/x-pack/plugins/lists/common/schemas/types/comments.ts new file mode 100644 index 0000000000000..d61608c3508f4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/comments.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; + +export const comment = t.exact( + t.type({ + comment: t.string, + created_at: t.string, // TODO: Make this into an ISO Date string check, + created_by: t.string, + }) +); + +export const commentsArray = t.array(comment); +export type CommentsArray = t.TypeOf; +export type Comment = t.TypeOf; +export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]); +export type CommentsArrayOrUndefined = t.TypeOf; + +export const commentPartial = t.intersection([ + t.exact( + t.type({ + comment: t.string, + }) + ), + t.exact( + t.partial({ + created_at: t.string, // TODO: Make this into an ISO Date string check, + created_by: t.string, + }) + ), +]); + +export const commentsPartialArray = t.array(commentPartial); +export type CommentsPartialArray = t.TypeOf; +export type CommentPartial = t.TypeOf; +export const commentsPartialArrayOrUndefined = t.union([commentsPartialArray, t.undefined]); +export type CommentsPartialArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts new file mode 100644 index 0000000000000..a80bb968561f0 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +import { CommentsArray, CommentsPartialArray, comment, commentPartial } from './comments'; + +export type DefaultCommentsArrayC = t.Type; +export type DefaultCommentsPartialArrayC = t.Type< + CommentsPartialArray, + CommentsPartialArray, + unknown +>; + +/** + * Types the DefaultCommentsArray as: + * - If null or undefined, then a default array of type entry will be set + */ +export const DefaultCommentsArray: DefaultCommentsArrayC = new t.Type< + CommentsArray, + CommentsArray, + unknown +>( + 'DefaultCommentsArray', + t.array(comment).is, + (input): Either => + input == null ? t.success([]) : t.array(comment).decode(input), + t.identity +); + +/** + * Types the DefaultCommentsPartialArray as: + * - If null or undefined, then a default array of type entry will be set + */ +export const DefaultCommentsPartialArray: DefaultCommentsPartialArrayC = new t.Type< + CommentsPartialArray, + CommentsPartialArray, + unknown +>( + 'DefaultCommentsPartialArray', + t.array(commentPartial).is, + (input): Either => + input == null ? t.success([]) : t.array(commentPartial).decode(input), + t.identity +); diff --git a/x-pack/plugins/lists/common/schemas/types/index.ts b/x-pack/plugins/lists/common/schemas/types/index.ts index 2f38ff86d4fb2..8e4b28b31d95c 100644 --- a/x-pack/plugins/lists/common/schemas/types/index.ts +++ b/x-pack/plugins/lists/common/schemas/types/index.ts @@ -3,5 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +export * from './default_comments_array'; export * from './default_entries_array'; +export * from './comments'; export * from './entries'; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index e914d816b5e91..2cafd435e0853 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -44,7 +44,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => { _tags, tags, meta, - comment, + comments, description, entries, item_id: itemId, @@ -76,7 +76,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => { } else { const createdList = await exceptionLists.createExceptionListItem({ _tags, - comment, + comments, description, entries, itemId, diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 14b97bbe15206..73392c326056e 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -45,7 +45,7 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { meta, type, _tags, - comment, + comments, entries, item_id: itemId, namespace_type: namespaceType, @@ -54,7 +54,7 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { const exceptionLists = getExceptionListClient(context); const exceptionListItem = await exceptionLists.updateExceptionListItem({ _tags, - comment, + comments, description, entries, id, diff --git a/x-pack/plugins/lists/server/saved_objects/exception_list.ts b/x-pack/plugins/lists/server/saved_objects/exception_list.ts index 9e1a708e0c83b..8fb618c01213c 100644 --- a/x-pack/plugins/lists/server/saved_objects/exception_list.ts +++ b/x-pack/plugins/lists/server/saved_objects/exception_list.ts @@ -66,9 +66,18 @@ export const exceptionListMapping: SavedObjectsType['mappings'] = { export const exceptionListItemMapping: SavedObjectsType['mappings'] = { properties: { - comment: { - // TODO: investigate what the deep mapping structure of this really is - type: 'keyword', + comments: { + properties: { + comment: { + type: 'keyword', + }, + created_at: { + type: 'keyword', + }, + created_by: { + type: 'keyword', + }, + }, }, entries: { properties: { diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json index c89c7a8f080cf..f1a0dd0fa9c91 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_auto_id.json @@ -5,7 +5,7 @@ "type": "simple", "description": "This is a sample endpoint type exception that has no item_id so it creates a new id each time", "name": "Sample Endpoint Exception List", - "comment": [], + "comments": [], "entries": [ { "field": "actingProcess.file.signer", diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_detection_auto_id.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_detection_auto_id.json index 3fe4458a73769..ad3fef7e14364 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_detection_auto_id.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_detection_auto_id.json @@ -5,7 +5,7 @@ "type": "simple", "description": "This is a sample detection type exception that has no item_id so it creates a new id each time", "name": "Sample Detection Exception List Item", - "comment": [], + "comments": [{ "comment": "This is a short little comment." }], "entries": [ { "field": "host.name", diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json index a53079318edfa..27f020c43d1bf 100644 --- a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json +++ b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json @@ -5,6 +5,14 @@ "type": "simple", "description": "This is a sample change here this list", "name": "Sample Endpoint Exception List update change", + "comments": [ + { + "comment": "this was an old comment.", + "created_by": "lily", + "created_at": "2020-04-20T15:25:31.830Z" + }, + { "comment": "this is a newly added comment" } + ], "entries": [ { "field": "event.category", diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts index c6d4bc006ef0b..f6a3bca10028d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts @@ -53,7 +53,7 @@ export const createExceptionList = async ({ const dateNow = new Date().toISOString(); const savedObject = await savedObjectsClient.create(savedObjectType, { _tags, - comment: undefined, + comments: undefined, created_at: dateNow, created_by: user, description, diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts index 44e87ab06f52b..22a9fbcfb53af 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts @@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import uuid from 'uuid'; import { - CommentOrUndefined, + CommentsPartialArray, Description, EntriesArray, ExceptionListItemSchema, @@ -23,11 +23,15 @@ import { _Tags, } from '../../../common/schemas'; -import { getSavedObjectType, transformSavedObjectToExceptionListItem } from './utils'; +import { + getSavedObjectType, + transformComments, + transformSavedObjectToExceptionListItem, +} from './utils'; interface CreateExceptionListItemOptions { _tags: _Tags; - comment: CommentOrUndefined; + comments: CommentsPartialArray; listId: ListId; itemId: ItemId; savedObjectsClient: SavedObjectsClientContract; @@ -44,7 +48,7 @@ interface CreateExceptionListItemOptions { export const createExceptionListItem = async ({ _tags, - comment, + comments, entries, itemId, listId, @@ -62,7 +66,7 @@ export const createExceptionListItem = async ({ const dateNow = new Date().toISOString(); const savedObject = await savedObjectsClient.create(savedObjectType, { _tags, - comment, + comments: transformComments({ comments, user }), created_at: dateNow, created_by: user, description, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index efd117a3c38f4..73c52fb8b3ec9 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -133,7 +133,7 @@ export class ExceptionListClient { public createExceptionListItem = async ({ _tags, - comment, + comments, description, entries, itemId, @@ -147,7 +147,7 @@ export class ExceptionListClient { const { savedObjectsClient, user } = this; return createExceptionListItem({ _tags, - comment, + comments, description, entries, itemId, @@ -164,7 +164,7 @@ export class ExceptionListClient { public updateExceptionListItem = async ({ _tags, - comment, + comments, description, entries, id, @@ -178,7 +178,7 @@ export class ExceptionListClient { const { savedObjectsClient, user } = this; return updateExceptionListItem({ _tags, - comment, + comments, description, entries, id, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 0ac543afee9f9..03f5de516561b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { - CommentOrUndefined, + CommentsPartialArray, Description, DescriptionOrUndefined, EntriesArray, @@ -88,7 +88,7 @@ export interface GetExceptionListItemOptions { export interface CreateExceptionListItemOptions { _tags: _Tags; - comment: CommentOrUndefined; + comments: CommentsPartialArray; entries: EntriesArray; itemId: ItemId; listId: ListId; @@ -102,7 +102,7 @@ export interface CreateExceptionListItemOptions { export interface UpdateExceptionListItemOptions { _tags: _TagsOrUndefined; - comment: CommentOrUndefined; + comments: CommentsPartialArray; entries: EntriesArrayOrUndefined; id: IdOrUndefined; itemId: ItemIdOrUndefined; diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts index 6a8fbf3306971..899ed30863770 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts @@ -18,7 +18,7 @@ import { } from '../../../common/schemas'; import { SavedObjectType } from '../../saved_objects'; -import { getSavedObjectType, transformSavedObjectsToFounExceptionList } from './utils'; +import { getSavedObjectType, transformSavedObjectsToFoundExceptionList } from './utils'; interface FindExceptionListOptions { namespaceType: NamespaceType; @@ -48,7 +48,7 @@ export const findExceptionList = async ({ sortOrder, type: savedObjectType, }); - return transformSavedObjectsToFounExceptionList({ namespaceType, savedObjectsFindResponse }); + return transformSavedObjectsToFoundExceptionList({ namespaceType, savedObjectsFindResponse }); }; export const getExceptionListFilter = ({ diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts index c3b09a5f44b15..1c3103ad1db7e 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts @@ -19,7 +19,7 @@ import { } from '../../../common/schemas'; import { SavedObjectType } from '../../saved_objects'; -import { getSavedObjectType, transformSavedObjectsToFounExceptionListItem } from './utils'; +import { getSavedObjectType, transformSavedObjectsToFoundExceptionListItem } from './utils'; import { getExceptionList } from './get_exception_list'; interface FindExceptionListItemOptions { @@ -61,7 +61,7 @@ export const findExceptionListItem = async ({ sortOrder, type: savedObjectType, }); - return transformSavedObjectsToFounExceptionListItem({ + return transformSavedObjectsToFoundExceptionListItem({ namespaceType, savedObjectsFindResponse, }); diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts index 39c319a944e38..7ca9bfd83ab64 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { - CommentOrUndefined, + CommentsPartialArray, DescriptionOrUndefined, EntriesArrayOrUndefined, ExceptionListItemSchema, @@ -22,12 +22,16 @@ import { _TagsOrUndefined, } from '../../../common/schemas'; -import { getSavedObjectType, transformSavedObjectUpdateToExceptionListItem } from './utils'; +import { + getSavedObjectType, + transformComments, + transformSavedObjectUpdateToExceptionListItem, +} from './utils'; import { getExceptionListItem } from './get_exception_list_item'; interface UpdateExceptionListItemOptions { id: IdOrUndefined; - comment: CommentOrUndefined; + comments: CommentsPartialArray; _tags: _TagsOrUndefined; name: NameOrUndefined; description: DescriptionOrUndefined; @@ -44,7 +48,7 @@ interface UpdateExceptionListItemOptions { export const updateExceptionListItem = async ({ _tags, - comment, + comments, entries, id, savedObjectsClient, @@ -72,7 +76,7 @@ export const updateExceptionListItem = async ({ exceptionListItem.id, { _tags, - comment, + comments: transformComments({ comments, user }), description, entries, meta, diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index 82a98f4bdd3e2..5690a42bed87e 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -7,6 +7,8 @@ import { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server'; import { + CommentsArrayOrUndefined, + CommentsPartialArrayOrUndefined, ExceptionListItemSchema, ExceptionListSchema, ExceptionListSoSchema, @@ -125,7 +127,7 @@ export const transformSavedObjectToExceptionListItem = ({ const { attributes: { _tags, - comment, + comments, created_at, created_by, description, @@ -147,7 +149,7 @@ export const transformSavedObjectToExceptionListItem = ({ // TODO: Do a throw if item_id or entries is not defined. return { _tags, - comment, + comments: comments ?? [], created_at, created_by, description, @@ -179,7 +181,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({ const { attributes: { _tags, - comment, + comments, description, entries, meta, @@ -196,7 +198,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({ // TODO: Do a throw if after the decode this is not the correct "list_type: list" return { _tags: _tags ?? exceptionListItem._tags, - comment: comment ?? exceptionListItem.comment, + comments: comments ?? exceptionListItem.comments, created_at: exceptionListItem.created_at, created_by: exceptionListItem.created_by, description: description ?? exceptionListItem.description, @@ -215,7 +217,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({ }; }; -export const transformSavedObjectsToFounExceptionListItem = ({ +export const transformSavedObjectsToFoundExceptionListItem = ({ savedObjectsFindResponse, namespaceType, }: { @@ -232,7 +234,7 @@ export const transformSavedObjectsToFounExceptionListItem = ({ }; }; -export const transformSavedObjectsToFounExceptionList = ({ +export const transformSavedObjectsToFoundExceptionList = ({ savedObjectsFindResponse, namespaceType, }: { @@ -248,3 +250,24 @@ export const transformSavedObjectsToFounExceptionList = ({ total: savedObjectsFindResponse.total, }; }; + +export const transformComments = ({ + comments, + user, +}: { + comments: CommentsPartialArrayOrUndefined; + user: string; +}): CommentsArrayOrUndefined => { + const dateNow = new Date().toISOString(); + if (comments != null) { + return comments.map((comment) => { + return { + comment: comment.comment, + created_at: comment.created_at ?? dateNow, + created_by: comment.created_by ?? user, + }; + }); + } else { + return comments; + } +}; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 2893c7dc961f2..d86a84996a34f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -439,7 +439,7 @@ describe('Exception helpers', () => { describe('#getFormattedComments', () => { test('it returns formatted comment object with username and timestamp', () => { - const payload = getExceptionItemMock().comment; + const payload = getExceptionItemMock().comments; const result = getFormattedComments(payload); expect(result[0].username).toEqual('user_name'); @@ -447,7 +447,7 @@ describe('Exception helpers', () => { }); test('it returns formatted timeline icon with comment users initial', () => { - const payload = getExceptionItemMock().comment; + const payload = getExceptionItemMock().comments; const result = getFormattedComments(payload); const wrapper = mount(result[0].timelineIcon as React.ReactElement); @@ -456,7 +456,7 @@ describe('Exception helpers', () => { }); test('it returns comment text', () => { - const payload = getExceptionItemMock().comment; + const payload = getExceptionItemMock().comments; const result = getFormattedComments(payload); const wrapper = mount(result[0].children as React.ReactElement); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 155c8a3e2926c..9bc39d72322ae 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -184,9 +184,9 @@ export const getDescriptionListContent = ( */ export const getFormattedComments = (comments: Comment[]): EuiCommentProps[] => comments.map((comment) => ({ - username: comment.user, - timestamp: moment(comment.timestamp).format('on MMM Do YYYY @ HH:mm:ss'), + username: comment.created_by, + timestamp: moment(comment.created_at).format('on MMM Do YYYY @ HH:mm:ss'), event: i18n.COMMENT_EVENT, - timelineIcon: , + timelineIcon: , children: {comment.comment}, })); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/mocks.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/mocks.ts index 0dba3fd26c487..f5577560a680a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/mocks.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/mocks.ts @@ -63,10 +63,10 @@ export const getExceptionItemMock = (): ExceptionListItemSchema => ({ namespace_type: 'single', name: '', description: 'This is a description', - comment: [ + comments: [ { - user: 'user_name', - timestamp: '2020-04-23T00:19:13.289Z', + created_by: 'user_name', + created_at: '2020-04-23T00:19:13.289Z', comment: 'Comment goes here', }, ], diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index d60d1ef71e502..86485e308b59f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -57,8 +57,8 @@ export interface DescriptionListItem { } export interface Comment { - user: string; - timestamp: string; + created_by: string; + created_at: string; comment: string; } @@ -106,7 +106,7 @@ export interface ExceptionsPagination { // TODO: Delete once types are updated export interface ExceptionListItemSchema { _tags: string[]; - comment: Comment[]; + comments: Comment[]; created_at: string; created_by: string; description?: string; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx index c5d2ffc7ac2bf..b49615e24d052 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx @@ -24,7 +24,7 @@ describe('ExceptionDetails', () => { test('it renders no comments button if no comments exist', () => { const exceptionItem = getExceptionItemMock(); - exceptionItem.comment = []; + exceptionItem.comments = []; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> @@ -77,15 +77,15 @@ describe('ExceptionDetails', () => { test('it renders comments plural if more than one', () => { const exceptionItem = getExceptionItemMock(); - exceptionItem.comment = [ + exceptionItem.comments = [ { - user: 'user_1', - timestamp: '2020-04-23T00:19:13.289Z', + created_by: 'user_1', + created_at: '2020-04-23T00:19:13.289Z', comment: 'Comment goes here', }, { - user: 'user_2', - timestamp: '2020-04-23T00:19:13.289Z', + created_by: 'user_2', + created_at: '2020-04-23T00:19:13.289Z', comment: 'Comment goes here', }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx index ce7d1d499de6e..7144bf8044704 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx @@ -49,9 +49,8 @@ const ExceptionDetailsComponent = ({ ); const commentsSection = useMemo((): JSX.Element => { - // TODO: return back to exceptionItem.comments once updated - const { comment } = exceptionItem; - if (comment.length > 0) { + const { comments } = exceptionItem; + if (comments.length > 0) { return ( - {!showComments ? i18n.COMMENTS_SHOW(comment.length) : i18n.COMMENTS_HIDE(comment.length)} + {!showComments + ? i18n.COMMENTS_SHOW(comments.length) + : i18n.COMMENTS_HIDE(comments.length)} ); } else { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx index de214b098d4ea..3eb333966f267 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx @@ -21,7 +21,7 @@ storiesOf('Components|ExceptionItem', module) .add('with os', () => { const payload = getExceptionItemMock(); payload.description = ''; - payload.comment = []; + payload.comments = []; payload.entries = [ { field: 'actingProcess.file.signer', @@ -44,7 +44,7 @@ storiesOf('Components|ExceptionItem', module) .add('with description', () => { const payload = getExceptionItemMock(); payload._tags = []; - payload.comment = []; + payload.comments = []; payload.entries = [ { field: 'actingProcess.file.signer', @@ -91,7 +91,7 @@ storiesOf('Components|ExceptionItem', module) const payload = getExceptionItemMock(); payload._tags = []; payload.description = ''; - payload.comment = []; + payload.comments = []; return ( { - // TODO: return back to exceptionItem.comments once updated - return getFormattedComments(exceptionItem.comment); + return getFormattedComments(exceptionItem.comments); }, [exceptionItem]); const disableDelete = useMemo((): boolean => {