diff --git a/packages/kbn-securitysolution-list-hooks/src/mocks/constants.mock.ts b/packages/kbn-securitysolution-list-hooks/src/mocks/constants.mock.ts index 075373cf1120f..8901f2183c13a 100644 --- a/packages/kbn-securitysolution-list-hooks/src/mocks/constants.mock.ts +++ b/packages/kbn-securitysolution-list-hooks/src/mocks/constants.mock.ts @@ -5,6 +5,15 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { + CommentsArray, + EntriesArray, + Entry, + EntryMatch, + EntryNested, + OsTypeArray, +} from '@kbn/securitysolution-io-ts-list-types'; + export const DATE_NOW = '2020-04-20T15:25:31.830Z'; export const USER = 'some user'; export const ELASTIC_USER = 'elastic'; @@ -18,3 +27,73 @@ export const TYPE = 'ip'; export const VERSION = 1; export const IMMUTABLE = false; +// Exception List specific +export const ID = 'uuid_here'; +export const NAMESPACE_TYPE = 'single'; +export const OS_TYPES: OsTypeArray = ['windows']; +export const TAGS = []; +export const UPDATED_COMMENTS = [ + { + comment: 'old comment', + id: 'old_created_id', + }, + { + comment: 'new comment', + id: 'new_id', + }, +]; +export const COMMENTS = []; +export const ENTRIES: EntriesArray = [ + { + 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' }, +]; +export const ITEM_ID = 'some-list-item-id'; +export const ITEM_TYPE = 'simple'; +export const LIST_ITEM_ID = 'some-list-item-id'; +// ENTRIES_WITH_IDS should only be used to mock out functionality of a collection of transforms +// that are UI specific and useful for UI concerns that are inserted between the +// API and the actual user interface. In some ways these might be viewed as +// technical debt or to compensate for the differences and preferences +// of how ReactJS might prefer data vs. how we want to model data. +export const ENTRIES_WITH_IDS: EntriesArray = [ + { + entries: [ + { + field: 'nested.field', + id: '123', + operator: 'included', + type: 'match', + value: 'some value', + } as EntryMatch & { id: string }, + ], + field: 'some.parentField', + id: '123', + type: 'nested', + } as EntryNested & { id: string }, + { + field: 'some.not.nested.field', + id: '123', + operator: 'included', + type: 'match', + value: 'some value', + } as Entry & { id: string }, +]; + +export const COMMENTS_WITH_CREATEDAT_CREATEDBY: CommentsArray = [ + { + comment: 'old comment', + id: 'old_created_id', + created_at: '2022-01-08T15:25:31.830Z', + created_by: 'elastic', + }, + { + comment: 'new comment', + id: 'new_id', + created_at: '2022-05-14T15:25:31.830Z', + created_by: 'elastic', + }, +]; diff --git a/packages/kbn-securitysolution-list-hooks/src/mocks/request/create_exception_list_item_schema.mock.ts b/packages/kbn-securitysolution-list-hooks/src/mocks/request/create_exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..ff7505cbff2a5 --- /dev/null +++ b/packages/kbn-securitysolution-list-hooks/src/mocks/request/create_exception_list_item_schema.mock.ts @@ -0,0 +1,63 @@ +/* + * 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 type { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +import { + COMMENTS, + DESCRIPTION, + ENTRIES, + ITEM_ID, + ITEM_TYPE, + LIST_ID, + META, + NAME, + NAMESPACE_TYPE, + OS_TYPES, + TAGS, +} from '../constants.mock'; + +export const getCreateExceptionListItemSchemaMock = (): CreateExceptionListItemSchema => ({ + comments: COMMENTS, + description: DESCRIPTION, + entries: ENTRIES, + item_id: undefined, + list_id: LIST_ID, + meta: META, + name: NAME, + namespace_type: NAMESPACE_TYPE, + os_types: OS_TYPES, + tags: TAGS, + type: ITEM_TYPE, +}); + +/** + * Useful for end to end testing + */ +export const getCreateExceptionListItemMinimalSchemaMock = (): CreateExceptionListItemSchema => ({ + description: DESCRIPTION, + entries: ENTRIES, + item_id: ITEM_ID, + list_id: LIST_ID, + name: NAME, + os_types: OS_TYPES, + type: ITEM_TYPE, +}); + +/** + * Useful for end to end testing + */ +export const getCreateExceptionListItemMinimalSchemaMockWithoutId = + (): CreateExceptionListItemSchema => ({ + description: DESCRIPTION, + entries: ENTRIES, + list_id: LIST_ID, + name: NAME, + os_types: OS_TYPES, + type: ITEM_TYPE, + }); diff --git a/packages/kbn-securitysolution-list-hooks/src/mocks/request/update_exception_list_item_schema.mock.ts b/packages/kbn-securitysolution-list-hooks/src/mocks/request/update_exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..f2f56c2d13881 --- /dev/null +++ b/packages/kbn-securitysolution-list-hooks/src/mocks/request/update_exception_list_item_schema.mock.ts @@ -0,0 +1,52 @@ +/* + * 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 type { UpdateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +import { + DESCRIPTION, + ENTRIES, + ID, + ITEM_ID, + ITEM_TYPE, + LIST_ITEM_ID, + META, + NAME, + NAMESPACE_TYPE, + OS_TYPES, + TAGS, + UPDATED_COMMENTS, +} from '../constants.mock'; + +export const getUpdateExceptionListItemSchemaMock = (): UpdateExceptionListItemSchema => ({ + _version: undefined, + comments: UPDATED_COMMENTS, + description: DESCRIPTION, + entries: ENTRIES, + id: ID, + item_id: LIST_ITEM_ID, + meta: META, + name: NAME, + namespace_type: NAMESPACE_TYPE, + os_types: ['linux'], + tags: TAGS, + type: ITEM_TYPE, +}); + +/** + * Useful for end to end tests and other mechanisms which want to fill in the values + * after doing a get of the structure. + */ +export const getUpdateMinimalExceptionListItemSchemaMock = (): UpdateExceptionListItemSchema => ({ + description: DESCRIPTION, + entries: ENTRIES, + item_id: ITEM_ID, + name: NAME, + os_types: OS_TYPES, + type: ITEM_TYPE, +}); diff --git a/packages/kbn-securitysolution-list-hooks/src/mocks/response/exception_list_item_schema.mock.ts b/packages/kbn-securitysolution-list-hooks/src/mocks/response/exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..4ba6066564d0d --- /dev/null +++ b/packages/kbn-securitysolution-list-hooks/src/mocks/response/exception_list_item_schema.mock.ts @@ -0,0 +1,70 @@ +/* + * 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 type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +import { + COMMENTS, + DATE_NOW, + DESCRIPTION, + ELASTIC_USER, + ENTRIES, + ITEM_ID, + ITEM_TYPE, + LIST_ID, + META, + NAME, + NAMESPACE_TYPE, + OS_TYPES, + TIE_BREAKER, + USER, +} from '../constants.mock'; + +export const getExceptionListItemSchemaMock = ( + overrides?: Partial +): ExceptionListItemSchema => ({ + _version: undefined, + comments: COMMENTS, + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + entries: ENTRIES, + id: '1', + item_id: 'endpoint_list_item', + list_id: 'endpoint_list_id', + meta: META, + name: NAME, + namespace_type: NAMESPACE_TYPE, + os_types: [], + tags: ['user added string for a tag', 'malware'], + tie_breaker_id: TIE_BREAKER, + type: ITEM_TYPE, + updated_at: DATE_NOW, + updated_by: USER, + ...(overrides || {}), +}); + +/** + * This is useful for end to end tests where we remove the auto generated parts for comparisons + * such as created_at, updated_at, and id. + */ +export const getExceptionListItemResponseMockWithoutAutoGeneratedValues = + (): Partial => ({ + comments: [], + created_by: ELASTIC_USER, + description: DESCRIPTION, + entries: ENTRIES, + item_id: ITEM_ID, + list_id: LIST_ID, + name: NAME, + namespace_type: 'single', + os_types: OS_TYPES, + tags: [], + type: ITEM_TYPE, + updated_by: ELASTIC_USER, + }); diff --git a/packages/kbn-securitysolution-list-hooks/src/transforms/index.test.ts b/packages/kbn-securitysolution-list-hooks/src/transforms/index.test.ts index 9b6bbf5908c3f..aab7dfd7b2b70 100644 --- a/packages/kbn-securitysolution-list-hooks/src/transforms/index.test.ts +++ b/packages/kbn-securitysolution-list-hooks/src/transforms/index.test.ts @@ -6,9 +6,293 @@ * Side Public License, v 1. */ +import type { + CreateExceptionListItemSchema, + Entry, + EntryMatch, + EntryNested, + ExceptionListItemSchema, + UpdateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { + addIdToExceptionItemEntries, + removeIdFromExceptionItemsEntries, + transformInput, + transformOutput, +} from '@kbn/securitysolution-list-hooks'; + +import { getCreateExceptionListItemSchemaMock } from '../mocks/request/create_exception_list_item_schema.mock'; +import { getUpdateExceptionListItemSchemaMock } from '../mocks/request/update_exception_list_item_schema.mock'; +import { getExceptionListItemSchemaMock } from '../mocks/response/exception_list_item_schema.mock'; +import { COMMENTS_WITH_CREATEDAT_CREATEDBY, ENTRIES_WITH_IDS } from '../mocks/constants.mock'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('123'), +})); + describe('Exceptions transforms', () => { - test('Tests should be ported', () => { - // TODO: Port all the tests from: x-pack/plugins/lists/public/exceptions/transforms.test.ts here once mocks are figured out and kbn package mocks are figured out - expect(true).toBe(true); + describe('transformOutput', () => { + it('should return same output as input with stripped ids per entry - CreateExceptionListItemSchema', () => { + const mockCreateExceptionItem = { + ...getCreateExceptionListItemSchemaMock(), + entries: ENTRIES_WITH_IDS, + }; + const output = transformOutput(mockCreateExceptionItem); + const expectedOutput: CreateExceptionListItemSchema = getCreateExceptionListItemSchemaMock(); + + expect(output).toEqual(expectedOutput); + }); + + it('should return same output as input with stripped ids per entry - UpdateExceptionListItemSchema', () => { + const mockUpdateExceptionItem = { + ...getUpdateExceptionListItemSchemaMock(), + entries: ENTRIES_WITH_IDS, + }; + const output = transformOutput(mockUpdateExceptionItem); + const expectedOutput: UpdateExceptionListItemSchema = getUpdateExceptionListItemSchemaMock(); + + expect(output).toEqual(expectedOutput); + }); + it('should return output as input with stripped createdAt and createdBy per entry - UpdateExceptionListItemSchema', () => { + const mockUpdateExceptionItem = { + ...getUpdateExceptionListItemSchemaMock(), + entries: ENTRIES_WITH_IDS, + comments: COMMENTS_WITH_CREATEDAT_CREATEDBY, + }; + const output = transformOutput(mockUpdateExceptionItem); + const expectedOutput: UpdateExceptionListItemSchema = getUpdateExceptionListItemSchemaMock(); + + expect(output).toEqual(expectedOutput); + }); + }); + + describe('transformInput', () => { + it('should return same output as input with added ids per entry', () => { + const mockExceptionItem = getExceptionListItemSchemaMock(); + const output = transformInput(mockExceptionItem); + const expectedOutput: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: ENTRIES_WITH_IDS, + }; + + expect(output).toEqual(expectedOutput); + }); + }); + + describe('addIdToExceptionItemEntries', () => { + it('should return same output as input with added ids per entry', () => { + const mockExceptionItem: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [ + { + field: 'some.not.nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + }; + const output = addIdToExceptionItemEntries(mockExceptionItem); + const expectedOutput: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [ + { + field: 'some.not.nested.field', + id: '123', + operator: 'included', + type: 'match', + value: 'some value', + } as Entry & { id: string }, + ], + }; + + expect(output).toEqual(expectedOutput); + }); + + it('should return same output as input with added ids per nested entry', () => { + const mockExceptionItem: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [ + { + entries: [ + { + field: 'nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + ], + }; + const output = addIdToExceptionItemEntries(mockExceptionItem); + const expectedOutput: ExceptionListItemSchema = { + ...getExceptionListItemSchemaMock(), + entries: [ + { + entries: [ + { + field: 'nested.field', + id: '123', + operator: 'included', + type: 'match', + value: 'some value', + } as EntryMatch & { id: string }, + ], + field: 'some.parentField', + id: '123', + type: 'nested', + } as EntryNested & { id: string }, + ], + }; + + expect(output).toEqual(expectedOutput); + }); + }); + + describe('removeIdFromExceptionItemsEntries', () => { + it('should return same output as input with stripped ids per entry - CreateExceptionListItemSchema', () => { + const mockCreateExceptionItem = { + ...getCreateExceptionListItemSchemaMock(), + entries: [ + { + field: 'some.not.nested.field', + id: '123', + operator: 'included', + type: 'match', + value: 'some value', + } as Entry & { id: string }, + ], + }; + const output = removeIdFromExceptionItemsEntries(mockCreateExceptionItem); + const expectedOutput: CreateExceptionListItemSchema = { + ...getCreateExceptionListItemSchemaMock(), + entries: [ + { + field: 'some.not.nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + }; + + expect(output).toEqual(expectedOutput); + }); + + it('should return same output as input with stripped ids per nested entry - CreateExceptionListItemSchema', () => { + const mockCreateExceptionItem = { + ...getCreateExceptionListItemSchemaMock(), + entries: [ + { + entries: [ + { + field: 'nested.field', + id: '123', + operator: 'included', + type: 'match', + value: 'some value', + } as EntryMatch & { id: string }, + ], + field: 'some.parentField', + id: '123', + type: 'nested', + } as EntryNested & { id: string }, + ], + }; + const output = removeIdFromExceptionItemsEntries(mockCreateExceptionItem); + const expectedOutput: CreateExceptionListItemSchema = { + ...getCreateExceptionListItemSchemaMock(), + entries: [ + { + entries: [ + { + field: 'nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + ], + }; + + expect(output).toEqual(expectedOutput); + }); + + it('should return same output as input with stripped ids per entry - UpdateExceptionListItemSchema', () => { + const mockUpdateExceptionItem = { + ...getUpdateExceptionListItemSchemaMock(), + entries: [ + { + field: 'some.not.nested.field', + id: '123', + operator: 'included', + type: 'match', + value: 'some value', + } as Entry & { id: string }, + ], + }; + const output = removeIdFromExceptionItemsEntries(mockUpdateExceptionItem); + const expectedOutput: UpdateExceptionListItemSchema = { + ...getUpdateExceptionListItemSchemaMock(), + entries: [ + { + field: 'some.not.nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + }; + + expect(output).toEqual(expectedOutput); + }); + + it('should return same output as input with stripped ids per nested entry - UpdateExceptionListItemSchema', () => { + const mockUpdateExceptionItem = { + ...getUpdateExceptionListItemSchemaMock(), + entries: [ + { + entries: [ + { + field: 'nested.field', + id: '123', + operator: 'included', + type: 'match', + value: 'some value', + } as EntryMatch & { id: string }, + ], + field: 'some.parentField', + id: '123', + type: 'nested', + } as EntryNested & { id: string }, + ], + }; + const output = removeIdFromExceptionItemsEntries(mockUpdateExceptionItem); + const expectedOutput: UpdateExceptionListItemSchema = { + ...getUpdateExceptionListItemSchemaMock(), + entries: [ + { + entries: [ + { + field: 'nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + ], + }; + + expect(output).toEqual(expectedOutput); + }); }); }); diff --git a/packages/kbn-securitysolution-list-hooks/src/transforms/index.ts b/packages/kbn-securitysolution-list-hooks/src/transforms/index.ts index 75652b9ab5f6f..89adb0c4953f6 100644 --- a/packages/kbn-securitysolution-list-hooks/src/transforms/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/transforms/index.ts @@ -36,7 +36,10 @@ import type { export const transformOutput = ( exceptionItem: UpdateExceptionListItemSchema | ExceptionListItemSchema ): UpdateExceptionListItemSchema | ExceptionListItemSchema => - flow(removeIdFromExceptionItemsEntries)(exceptionItem); + flow( + removeCreatedAtCreatedByFromCommentsOnUpdate, + removeIdFromExceptionItemsEntries + )(exceptionItem); export const transformNewItemOutput = ( exceptionItem: CreateExceptionListItemSchema @@ -112,3 +115,23 @@ export const removeIdFromExceptionItemsEntries = { + const { comments } = exceptionItem; + if (!comments || !comments.length) return exceptionItem; + + const entriesNoCreatedAtAndBy = comments.map(({ comment, id }) => ({ + comment, + id, + })); + return { ...exceptionItem, comments: entriesNoCreatedAtAndBy }; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx index bf74bf58ad07b..6bf0b87652e3e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx @@ -27,8 +27,8 @@ import type { ExceptionListSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { + updateExceptionListItemSchema, ExceptionListTypeEnum, - exceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import type { ExceptionsBuilderReturnExceptionItem } from '@kbn/securitysolution-list-utils'; @@ -237,7 +237,7 @@ const EditExceptionFlyoutComponent: React.FC = ({ const areItemsReadyForUpdate = useCallback( (items: ExceptionsBuilderReturnExceptionItem[]): items is ExceptionListItemSchema[] => { - return items.every((item) => exceptionListItemSchema.is(item)); + return items.every((item) => updateExceptionListItemSchema.is(item)); }, [] );